Самый простой способ кодирования функтора структурной карты в C ++ - PullRequest
3 голосов
/ 28 августа 2009

Это опрос мнений о наиболее читабельном способе сделать что-либо - использовать ли указатель на элемент C ++, смещение байтов или шаблонизированный функтор, чтобы определить «выбрать элемент X из структуры foo».

У меня есть тип, который содержит большой вектор структур, и я пишу вспомогательную функцию, которая в основном работает как сокращение в некотором диапазоне из них. Каждая структура связывает группу зависимых переменных с некоторой точкой вдоль независимого измерения - чтобы придумать упрощенный пример, представьте, что здесь записывается ряд условий окружающей среды для комнаты с течением времени:

// all examples are psuedocode for brevity
struct TricorderReadings
{
  float time;  // independent variable

  float tempurature;
  float lightlevel;
  float windspeed; 
  // etc for about twenty other kinds of data...
}

Моя функция просто выполняет кубическую интерполяцию , чтобы угадать эти условия для некоторого заданного момента времени между доступными выборками.

// performs Hermite interpolation between the four samples closest to given time
float TempuratureAtTime( float time, sorted_vector<TricorderReadings> &data)
{
    // assume all the proper bounds checking, etc. is in place
    int idx = FindClosestSampleBefore( time, data );
    return CubicInterp( time, 
                        data[idx-1].time, data[idx-1].tempurature,
                        data[idx+0].time, data[idx+0].tempurature,
                        data[idx+1].time, data[idx+1].tempurature,
                        data[idx+2].time, data[idx+2].tempurature );
}

Я бы хотел обобщить эту функцию, чтобы ее можно было применять в общем для любого элемента, а не только для температуры. Я могу придумать три способа сделать это, и хотя все они просты для кодирования, я не уверен, что будет наиболее читабельным для тех, кто будет использовать это через год. Вот что я рассматриваю:


Синтаксис указателя на член

typedef int TricorderReadings::* selector;
float ReadingAtTime( time, svec<TricorderReadings> &data, selector whichmember )
{
   int idx = FindClosestSampleBefore( time, data );
   return CubicInterp( time, data[idx-1].time, data[idx-1].*whichmember, 
                       /* ...etc */  );
}
// called like:
ReadingAtTime( 12.6f, data, &TricorderReadings::windspeed );

Это похоже на самый "C ++ y" способ сделать это, но выглядит странно, и весь синтаксис указателя на член используется редко и поэтому плохо понимается большинством людей в моей команде. Это технически «правильный» способ, но также тот, о котором я получу самые запутанные электронные письма.

Смещение структуры

float ReadingAtTime( time, svec<TricorderReadings> &data, int memberoffset )
{
   int idx = FindClosestSampleBefore( time, data );
   return CubicInterp( time, 
                       data[idx-1].time, 
                       *(float *) ( ((char *)(&data[idx-1]))+memberoffset ), 
                       /* ...etc */  );
}
// called like:
ReadingAtTime( 12.6f, data, offsetof(TricorderReadings, windspeed) );

Это функционально идентично приведенному выше, но делает указатель явно. Этот подход будет немедленно знаком и понятен всем в моей команде (кто все изучал C до C ++), и он надежен, но кажется просто неприличным.

Templatized Functor

template <class F>
float ReadingAtTime( time, svec<TricorderReadings> &data )
{
   int idx = FindClosestSampleBefore( time, data );
   return CubicInterp( time, 
                       data[idx-1].time, 
                       F::Get(data[idx-1]) ), 
                       /* ...etc */  );
}

// called with:
class WindSelector
{ 
   inline static float Get(const TricorderReadings &d) { return d.windspeed; }
}
ReadingAtTime<WindSelector>( 12.6f, data );

Это самый простой и STL-иш способ ведения дел, но кажется, что это целая куча дополнительных типов и синтаксиса, а также кратких определений классов. Он компилирует почти то же самое, что и два выше, но также выводит кучу избыточных определений функций по всему исполняемому файлу. (Я подтвердил это с помощью / FAcs , но, возможно, компоновщик снова их удалит.)


Все три вышеперечисленных будут работать, и компилятор выдает почти одинаковый код для всех них; Итак, самый важный выбор, который я должен сделать, это просто , который наиболее читабелен . Что ты думаешь?

Ответы [ 4 ]

3 голосов
/ 28 августа 2009

Я нахожу Templatized Functor очень ясным в этом случае.

ReadingAtTime<WindSelector>( 12.6f, data );
2 голосов
/ 28 августа 2009

Более простой способ STL - это универсальный функтор, который делает доступ через указатель на член похожим на вызов функции. Это может выглядеть примерно так:

#include <functional>

template <class T, class Result>
class member_pointer_t: public std::unary_function<T, Result>
{
    Result T::*member;
public:
    member_pointer_t(Result T::*m): member(m) {}
    Result operator()(const T& o) const { return o.*member; }
};

template <class T, class Result>
member_pointer_t<T, Result> member_pointer(Result T::*member)
{
    return member_pointer_t<T, Result>(member);
}

float ReadingAtTime( float time, const std::vector<TricorderReadings> &data, member_pointer_t<TricorderReadings, float> f )
{
   int idx = FindClosestSampleBefore( time, data );
   return CubicInterp( time, data[idx-1].time, f(data[idx-1]));
}

ReadingAtTime( 12.6f, data, &TricorderReadings::windspeed);

В этом примере также включена вспомогательная функция, помогающая определить аргументы шаблона для функтора (в этом примере не используется).

Функция ReadingAtTime также может принимать шаблонный функтор:

template <class Func>
float ReadingAtTime( float time, const std::vector<TricorderReadings>& data, Func f);

ReadingAtTime( 12.6f, data, member_pointer(&TricorderReadings::windspeed));

Таким образом, вы можете использовать все виды функций / функторов для получения значения из данных [idx - 1], а не только указатели на член.

Более общие эквиваленты member_pointer могут быть std :: tr1 :: bind или std :: tr1 :: mem_fn.

1 голос
/ 28 августа 2009

Для простых вещей я бы предпочел решение Pointer-to-member. Однако у функторного подхода есть два возможных преимущества:

  1. отделяя алгоритм от Данные позволяют использовать алгоритм для большего количества вещей в будущем, синус это работает с чем-либо при условии, что вы может построить правильный функтор.

  2. относится к # 1, это может сделать тестирование алгоритма проще, так как вы есть способ предоставить данные испытаний для функция, которая не включает создание полных объектов данных, которые вы намерены использовать. Вы можете использовать проще фиктивные объекты.

Однако, я думаю, что функторный подход стоит того, только если выполняемая вами функция очень сложна и / или используется во многих разных местах.

1 голос
/ 28 августа 2009

Если ваша команда состоит из достаточно умных людей, я бы сказал, доверять им и их способностям и использовать технически предпочтительное решение, которое предлагает синтаксис указатель на член. Это то, для чего это было сделано.

Если вы действительно обеспокоены, вы можете предпринять некоторые шаги, чтобы облегчить любые будущие проблемы,

  • Отмечая в комментарии рядом с typedef и использование, которое это называется синтаксисом "указатель на член", чтобы другие члены команды знали, что искать
  • Укажите это явно в обзоре кода, где многие из них должны присутствовать. Предложите изменить его, если он считается непонятным или слишком неясным для обслуживания.

У двух других подходов есть проблемы, как вы описали, так и далее:

  • Оба требуют больше кода, больше места для опечаток и т. Д.
  • Примитив offsetof ограничен тем, к каким типам он может применяться:

    Из-за расширенной функциональности структур в C ++ в этом языке использование offsetof ограничено «типами POD», которые для классов более или менее соответствуют концепции C структуры struct (хотя не производные классы с только общедоступные не виртуальные функции-члены и без конструктора и / или деструктора также квалифицируются как POD).

С здесь .

...