How to design a class with "annotated" fields?
Asked Answered
B

4

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.

Boathouse answered 27/3, 2012 at 20:20 Comment(2)
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.Wilder
@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.Boathouse
M
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]; 
  } 
};
Marginalia answered 27/3, 2012 at 20:40 Comment(6)
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).Boathouse
@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. https://mcmap.net/q/67293/-which-typesafe-enum-in-c-are-you-usingMarginalia
@KerrekSB Updated. It isn't fully working so far, but it seems the struct fields are being generated.Marginalia
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'.Boathouse
@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.Marginalia
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.Boathouse
H
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.

Herefordshire answered 27/3, 2012 at 20:34 Comment(0)
B
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; };
Boathouse answered 27/3, 2012 at 23:29 Comment(0)
S
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));
Subeditor answered 27/3, 2012 at 20:44 Comment(3)
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.Boathouse
@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)?Subeditor
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.Boathouse

© 2022 - 2024 — McMap. All rights reserved.