How to design a class with “annotated” fields?

孤街浪徒 提交于 2019-12-31 14:35:38

问题


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.


回答1:


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



回答2:


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.




回答3:


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



回答4:


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


来源:https://stackoverflow.com/questions/9897074/how-to-design-a-class-with-annotated-fields

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!