Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to design a class with "annotated" fields?

Imagine we have some sort of protocol with hundreds of message types, each of which we want to model by a C++ class. Since each class should be able to process each field automatically, a natural solution is to just have an std::tuple with all the required types:

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

print(message);   // the usual variadic magic

This is all fine and well. However, now I want to give each field a name, and I want to be able to use the name when referring to the field in my code, as well as get a textual representation of it. Naively, or in C, I might have written:

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

That way we lose the recursive automagic processing power of the tuple, but we can name each field literally. In C++, we can do both by means of an enum:

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;
};

Now I can say, Message m; m.get<Message::header>() = 12; etc., and I can recurse over the fields and make each print out their own value prefixed by their own name, etc.


Now the question: How can I author such code efficiently, without repetition?

Ideally, I want to be able to say this:

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

Is there any way, combining preprocessor, Boost and C++11, to achieve something like this without the need for external generation tools? (I think Boost.Preprocessor calls this "horizontal" and "vertical" repetition. I need to "transpose" the field data somehow.) The key feature here is that I never have to repeat any of the information, and that modifying or adding one field only requires one single change.

like image 751
Kerrek SB Avatar asked Mar 27 '12 20:03

Kerrek SB


2 Answers

You can do this with boost's preprocessor sequences.

#define CREATE_MESSAGE(NAME, SEQ) ...

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

You would need to iterate over each pair to generate the definitions. I don't have any example code handy, though I can probably arrange some if it is interesting.

At one point I had a generator for something like this that also generated all the serialization for the fields. I kind of felt like it went a little too far. I feel like concrete definitions and declarative visitors on the fields is more straight forward. It's a little less magical in case someone else had to maintain the code after me. I don't know you're situation obviously, just after implementing it I still had reservations. :)

It would be cool to look at again with the C++11 features, though I haven't had a chance.

Update:

There are still a few kinks to work out, but this is mostly working.

#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;
}

It is having problems with get's decltype. I haven't really used tuple to know what to expect there. I don't think it has anything to do with how you generate the types or fields, though.

Here is what the preprocessor is producing with -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]; 
  } 
};
like image 118
Tom Kerr Avatar answered Nov 19 '22 06:11

Tom Kerr


This isn't an answer, but merely another (scary) idea to consider. I have a inl file I wrote once that kinda sorta is vaguely similar. It's here: http://ideone.com/6CvgR

The basic concept is the caller does this:

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

and the inl file creates a custom bitfield type with named members by redefining SEPERATOR and then using BITTYPES again. Which can then be used easily, including a ToString function.

 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;

The inline file itself is terrifying code, but some approach vaguely like this might simplify the caller's usage.

If I have time later, I'll see if I can make something along this idea for what you're wanting.

like image 1
Mooing Duck Avatar answered Nov 19 '22 05:11

Mooing Duck