Как спроектировать класс с «аннотированными» полями? - PullRequest
9 голосов
/ 28 марта 2012

Представьте, что у нас есть своего рода протокол с сотнями типов сообщений, каждый из которых мы хотим смоделировать с помощью класса C ++. Поскольку каждый класс должен иметь возможность обрабатывать каждое поле автоматически, естественным решением будет просто иметь std::tuple со всеми необходимыми типами:

std::tuple<int, double, char> message;

print(message);   // the usual variadic magic

Это все хорошо и хорошо. Однако теперь я хочу дать каждому полю имя, и я хочу иметь возможность использовать имя при обращении к полю в моем коде, а также получить его текстовое представление. Наивно или на С я мог бы написать:

struct Message
{
    int    header;
    double temperature;
    char   flag;
};

Таким образом, мы теряем рекурсивную автоматическую вычислительную мощность кортежа, но мы можем назвать каждое поле буквально. В C ++ мы можем сделать оба с помощью перечисления:

struct Message
{
    enum FieldID { header, temperature, flag };
    static const char * FieldNames[] = { "header", "temperature", "flag" };

    typedef std::tuple<int, double, char> tuple_type;

    template <FieldID I>
    typename std::tuple_element<I, tuple_type>::type & get()
    { return std::get<I>(data); }

    template <FieldID I>
    static const char * name() { return FieldNames[I]; }

    tuple_type data;
};

Теперь я могу сказать, Message m; m.get<Message::header>() = 12; и т. Д., И я могу просмотреть все поля и заставить каждую распечатывать свое собственное значение с префиксом своего имени и т. Д.


Теперь вопрос: Как мне эффективно написать такой код без повторов?

В идеале, я хочу сказать следующее:

START_MESSAGE(Message)
ADDFIELD(int, header)
ADDFIELD(double, temperature)
ADDFIELD(char, flag)
END_MESSAGE

Есть ли способ, объединяющий препроцессор, Boost и C ++ 11, для достижения чего-то подобного без необходимости использования внешних инструментов генерации? (Я думаю, что Boost.Preprocessor вызывает это «горизонтальное» и «вертикальное» повторение. Мне нужно как-то «транспонировать» данные поля.) Ключевой особенностью здесь является то, что мне никогда не придется повторять какую-либо информацию, и это изменение или добавление одно поле требует только одного изменения.

Ответы [ 4 ]

3 голосов
/ 28 марта 2012

Вы можете сделать это с помощью последовательностей препроцессора boost.

#define CREATE_MESSAGE(NAME, SEQ) ...

CREATE_MESSAGE(SomeMessage,
  (int)(header)
  (double)(temperature)
  (char)(flag)
)

Вам потребуется перебирать каждую пару для генерации определений.У меня нет удобного примера кода, хотя я, возможно, смогу организовать его, если это будет интересно.

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

Было бы здорово взглянуть снова с функциями C ++ 11, хотя у меня не было шанса.

Обновление:

Есть ещенесколько изломов, чтобы работать, но это в основном работает.

#include <boost/preprocessor.hpp>
#include <boost/preprocessor/seq/for_each_i.hpp>
#include <boost/preprocessor/arithmetic/mod.hpp>
#include <boost/preprocessor/control/if.hpp>

#include <tuple>

#define PRIV_CR_FIELDS(r, data, i, elem) \
    BOOST_PP_IF(BOOST_PP_MOD(i, 2),elem BOOST_PP_COMMA,BOOST_PP_EMPTY)()

#define PRIV_CR_STRINGS(r, data, i, elem) \
    BOOST_PP_IF(BOOST_PP_MOD(i, 2),BOOST_PP_STRINGIZE(elem) BOOST_PP_COMMA,BOOST_P

#define PRIV_CR_TYPES(r, data, i, elem) \
    BOOST_PP_IF(BOOST_PP_MOD(i, 2),BOOST_PP_EMPTY,elem BOOST_PP_COMMA)()

#define CREATE_MESSAGE(NAME, SEQ) \
    struct NAME { \
        enum FieldID { \
            BOOST_PP_SEQ_FOR_EACH_I(PRIV_CR_FIELDS, _, SEQ) \
        }; \
        std::tuple< \
            BOOST_PP_SEQ_FOR_EACH_I(PRIV_CR_TYPES, _, SEQ) \
        > data;\
        template <FieldID I> \
            auto get() -> decltype(std::get<I>(data)) { \
                return std::get<I>(data); \
            } \
        template <FieldID I> \
            static const char * name() { \
                static constexpr char *FieldNames[] = { \
                    BOOST_PP_SEQ_FOR_EACH_I(PRIV_CR_STRINGS, _, SEQ) \
                }; \
                return FieldNames[I]; \
            } \
    };

CREATE_MESSAGE(foo,
        (int)(a)
        (float)(b)
    )

#undef CREATE_MESSAGE

int main(int argc, char ** argv) {

    foo f;
    f.get<foo::a>() = 12;

    return 0;
}

У него проблемы с getl типа decl.Я действительно не использовал кортеж, чтобы знать, чего ожидать там.Я не думаю, что это имеет какое-либо отношение к тому, как вы генерируете типы или поля.

Вот что препроцессор производит с -E:

struct foo { 
  enum FieldID { a , b , }; 
  std::tuple< int , float , > data;
  template <FieldID I> 
    auto get() -> decltype(std::get<I>(data)) { 
      return std::get<I>(data); 
  } 
  template <FieldID I> static const char * name() { 
    static constexpr char *FieldNames[] = { "a" , "b" , }; 
    return FieldNames[I]; 
  } 
};
1 голос
/ 28 марта 2012

Основываясь на предложении Тома Керра, я посмотрел последовательности Boost.Preprocessor.Вот что я придумал:

#include <boost/preprocessor/seq.hpp>
#include <boost/preprocessor/comma_if.hpp>
#include <boost/preprocessor/arithmetic.hpp>
#include <boost/preprocessor/stringize.hpp>

#include <tuple>

#define PROJECT1(a,b) a
#define PROJECT2(a,b) b

#define BOOST_TT_projectqu(r,data,t) BOOST_PP_COMMA_IF(BOOST_PP_SUB(r, 2)) BOOST_PP_STRINGIZE(PROJECT2 t)
#define BOOST_TT_project1(r,data,t) BOOST_PP_COMMA_IF(BOOST_PP_SUB(r, 2)) PROJECT1 t
#define BOOST_TT_project2(r,data,t) BOOST_PP_COMMA_IF(BOOST_PP_SUB(r, 2)) PROJECT2 t


template <typename T> struct Field { };

#define MESSAGE(classname, data) struct classname                                                \
  {                                                                                              \
      typedef std::tuple<BOOST_PP_SEQ_FOR_EACH(BOOST_TT_project1, ~, data)> tuple_type;          \
                                                                                                 \
      static constexpr char const * FieldNames[BOOST_PP_SEQ_SIZE(data)] = { BOOST_PP_SEQ_FOR_EACH(BOOST_TT_projectqu, ~, data) }; \
                                                                                                 \
      enum FieldID { BOOST_PP_SEQ_FOR_EACH(BOOST_TT_project2, ~, data) };                        \
                                                                                                 \
      template <FieldID I> using type = typename std::tuple_element<I, tuple_type>::type;        \
                                                                                                 \
      template <FieldID I> typename std::tuple_element<I, tuple_type>::type & get() { return std::get<I>(dat); } \
      template <FieldID I> typename std::tuple_element<I, tuple_type>::type const & get() const { return std::get<I>(dat); } \
                                                                                                 \
  private:                                                                                       \
      tuple_type dat;                                                                            \
  };

MESSAGE(message,            \
    ((int, header))         \
    ((double,temperature))  \
    ((char, flag))          \
)

Компиляция всей вещи с помощью gcc -std=c++11 -E -P (и переформатирование) дает:

template <typename T> struct Field { };

struct message {
    typedef std::tuple< int , double , char > tuple_type;
    static constexpr char const * FieldNames[3] = { "header" , "temperature" , "flag" };
    enum FieldID { header , temperature , flag };
    template <FieldID I> using type = typename std::tuple_element<I, tuple_type>::type;
    template <FieldID I> typename std::tuple_element<I, tuple_type>::type & get() { return std::get<I>(dat); }
    template <FieldID I> typename std::tuple_element<I, tuple_type>::type const & get() const { return std::get<I>(dat); }
    private: tuple_type dat; };
1 голос
/ 28 марта 2012

Это не ответ, а просто еще одна (страшная) идея для рассмотрения.У меня есть файл inl, который я однажды написал, что своего рода Сорта неопределенно похожа.Это здесь: http://ideone.com/6CvgR

Основная концепция заключается в том, что вызывающая сторона делает это:

#define BITNAME color
#define BITTYPES SEPERATOR(Red) SEPERATOR(Green) SEPERATOR(Blue)
#define BITTYPE unsigned char
#include "BitField.inl"

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

 colorBitfield Pixel;
 Pixel.BitField = 0; // sets all values to zero;
 Pixel.Green = 1; // activates green;
 std::cout << "Pixel.Bitfield=" << (int)Pixel.BitField << std::endl;  //this is machine dependant, probably 2 (010).
 Pixel.BitField |= (colorBitfield::GreenFlag | colorBitfield::BlueFlag); // enables Green and Blue
 std::cout << "BlueFlag=" << (Pixel.BitField & colorBitfield::BlueFlag) << std::endl; // 1, true.
 std::cout << "sizeof(colorBitField)=" << sizeof(colorBitfield) << std::endl;

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

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

0 голосов
/ 28 марта 2012

Вы можете сделать что-то похожее на то, что делает BOOST_SERIALIZATION_NVP (из библиотеки Boost.Serialization). Макрос создает (недолговечную) структуру-оболочку, которая связывает вместе имя своего аргумента и значение. Эта пара имя-значение затем обрабатывается библиотечным кодом (имя на самом деле важно только для сериализации XML, в противном случае оно отбрасывается).

Итак, ваш код может выглядеть так:

int    header      = 42;
double temperature = 36.6;
char   flag        = '+';
print (Message () + MY_NVP (header) + MY_NVP (temperature) + MY_NVP (flag));
...