问题
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