9

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.

Kerrek SB
  • 464,522
  • 92
  • 875
  • 1,084
  • 1
    For this kind of problems, a simple descriptive language and a custom preprocessor generating an include file is best in the long run. The "source file" is easily maintainable, and can even be generated by external tools should the project require it. – Alexandre C. Mar 27 '12 at 21:35
  • @AlexandreC.: At any given stage, "just one more small tool" always looks like the nicer answer. But in the grand scheme of things, you just have one more thing to carry with you and maintain and document and remember and train people on. Having something that works out of the box is definitely worth the pain of setting up some horrendous macros. – Kerrek SB Mar 27 '12 at 23:36

4 Answers4

3

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]; 
  } 
};
Tom Kerr
  • 10,444
  • 2
  • 30
  • 46
  • That sounds promising. Let me read the documentation of that. If you have a more complete code example, I'd be very grateful (and would perhaps send some bounty). – Kerrek SB Mar 27 '12 at 20:48
  • @KerrekSB Here is a reference to a link (boost vault) for someone trying to make stronger typed enums, which should show you would do the iteration over the sequence at least. http://stackoverflow.com/a/439004/839436 – Tom Kerr Mar 27 '12 at 20:54
  • @KerrekSB Updated. It isn't fully working so far, but it seems the struct fields are being generated. – Tom Kerr Mar 27 '12 at 22:21
  • Thanks a lot! I got it to work with that idea -- I'll post my solution, but I'll accept your answer as the crucial 'spark'. – Kerrek SB Mar 27 '12 at 23:23
  • @KerrekSB Good to hear, I really only was thinking of how to get the types, etc defined. I couldn't get the return type on get to work correctly. – Tom Kerr Mar 27 '12 at 23:28
  • That was my fault; the `auto` thing didn't actually work. I replaced it by the correct construction in the question, and also in my answer. – Kerrek SB Mar 27 '12 at 23:30
1

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.

Mooing Duck
  • 64,318
  • 19
  • 100
  • 158
1

Based on Tom Kerr's suggestion, I looked up Boost.Preprocessor sequences. Here's what I came up with:

#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))          \
)

Compiling the entire thing with gcc -std=c++11 -E -P (and reformatting) gives:

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; };
Kerrek SB
  • 464,522
  • 92
  • 875
  • 1,084
0

You could do something similar to what BOOST_SERIALIZATION_NVP (from Boost.Serialization library) does. The macro creates a (short-lived) wrapper structure that binds together the name of its argument and the value. This name-value pair is then processed by the library code (name is actually only important in XML serialization, otherwise it is discarded).

So, your code could look like:

int    header      = 42;
double temperature = 36.6;
char   flag        = '+';
print (Message () + MY_NVP (header) + MY_NVP (temperature) + MY_NVP (flag));
  • Hm, interesting... I was wondering if Boost.serialization might have any tools that could help with the job. But I really do would like the actual `Message` class around. I already have serialization code for it, so I might just want to instantiate one, populate it and send it to my serializer, for instance, or print its content to a log file. – Kerrek SB Mar 27 '12 at 20:46
  • @KerrekSB: I think I misunderstood you then. Do you want to keep `Message` class as it is now *and* make it easily possible to create similar classes too (`Message1`, ... `Message53`)? –  Mar 27 '12 at 20:51
  • Yes, indeed. I want to design many message classes (permanently), and I want to have an easy way to *author* all those classes. I could just write out each one manually as I did in the example, but that would be tedious and terrible. I could also write an external tool to create the class definitions, but that too would be very unsatisfactory. – Kerrek SB Mar 27 '12 at 20:59