Самый простой способ закодировать функтор карты structarray в C++

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

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

// 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++), и он надежен, но кажется непривлекательным.

Шаблонный функтор

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. , но, возможно, компоновщик снова удалит их.)


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


person Crashworks    schedule 28.08.2009    source источник


Ответы (4)


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

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

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

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

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

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

Из здесь.

person Phil Miller    schedule 28.08.2009
comment
Думаю, я пойду с этим. Вы хорошо заметили, что любой разработчик, достойный своей зарплаты, должен уметь немного изучить синтаксис, прочитав комментарий; и я думаю, что столкнусь с вооруженным восстанием со стороны команды, если заставлю их создавать крошечный класс на лету каждый раз, когда они хотят интерполировать переменную. - person Crashworks; 29.08.2009

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

ReadingAtTime<WindSelector>( 12.6f, data );
person AraK    schedule 28.08.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.

person UncleBens    schedule 28.08.2009

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

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

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

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

person MadCoder    schedule 28.08.2009