It's true that C++ does not support reflection as a language feature, but there are many ways that people implement reflection-like properties.
For one thing you can look at boost::fusion
-- in fusion, you can do things like, iterate over all the variables of a struct in order. Fusion has some templates and macros that allow you to take an arbitrary struct declared in one part of your program, and then do metaprogramming things with it as though it were a std::tuple
of its member variable types. There are some useful practical examples of doing this in the boost::spirit
documentation and examples.
Fusion however doesn't track the names of variables, just their types and their order.
If you want to be able to iterate over the list of type-and-name pairs over the variables in your struct at compile time, it can be done but as far as I know there is no library for it and you'll have to roll your own thing. (If there is a lib that does it I would like to know!)
In another project, I basically did this (in C++11) as follows:
- I make a type called
serial_struct_field
. This is a compile-time data structure, it is just a type with no members, which contains a typedef
called type
and a static constexpr
function of no arguments which returns a string literal called name
. You also should have a static constexpr
member pointer corresponding to the member variable. The purpose of this is to represent all the compile-time metadata you want to have about a member variable in some struct
.
(Actually serial_struct_field
is a concept rather than an actual type, there's no need for inheritance or anything here, as you see.)
- I make a type trait called "serial_struct". A serial struct (constructed using certain macros in my system) is one that can provide, via this trait, a typelist of
serial_struct_field
types. There are some template functions that check the trait and then can apply a visitor to each struct member if desired, so you can do reading / writing in many different ways as appropriate.
- For instance, you could make something like "lua_state_visitor" that can read or write primitive things like
int, double, std::string, std::vector<std::string>
to a lua_State
, and then you would use the visitor pattern like lua_state_visitor my_visitor{get_lua_state()}; my_serial_struct.write(my_visitor);
or something like this.
- Then you could have a different visitor for reading and writing json, etc.
- If you define it properly then you can have
serial_struct
inside your serial_struct
's and it will work fine. :)
To make a serial struct, I provide three macros BEGIN_SERIAL_STRUCT
, SERIAL_FIELD
and END_SERIAL_STRUCT
. In code it looks like this:
struct foo {
BEGIN_SERIAL_STRUCT(foo);
SERIAL_FIELD(std::string, id);
SERIAL_FIELD(int, my_int);
SERIAL_FIELD(double, my_double);
SERIAL_FIELD(std::vector<std::string>, my_vector);
END_SERIAL_STRUCT;
std::unique_ptr<bar> something_that_cant_really_be_serialized;
std::shared_ptr<foo_context> more_such_things;
};
The resulting structure is essentially the same as
struct foo {
std::string, id;
int my_int;
double my_double;
std::vector<std::string> my_vector;
std::unique_ptr<bar> something_that_cant_really_be_serialized;
std::shared_ptr<foo_context> more_such_things;
};
as far as run-time characteristics are concerned, but the macros and such set up some types and functions so that you can do the "generic serialization".
To implement the macros themselves as illustrated, you need to be able to accumulate a list of compile time information. That's a bit tricky but there are some tricks to do it without too much complication in C++11.
If you are willing to change the macro to look like this:
SERIAL_FIELDS(
(std::string, id),
(int, my_int),
(double, my_double),
(std::vector<std::string>, my_vector));
or something, then it is a lot simpler to implement, and you can basically do the code generation using only macros and not having to use helper templates.
(But, it's also a design consideration, templates are usually a bit more robust than macros and they don't always play well together. For instance if your types in your serial fields have multiple template parameters (and thus commas) that can confuse your macros and give you mind-numbing problems, so for a complicated / potentially going to become complicated application you might be better off with using the templates under the hood as much as possible...)
For the first version I used a technique sort of like what is described here, but changed so that it can all take place inside the definition of a struct
. For the second version, if you just want to iterate over a list within a macro, you can use known techniques like described here. Either version will be only a few hundred lines, and self-contained.
Basically you can have reflection if you really want in C++11, if you are willing to put up with a few hundred lines of boiler-plate hidden behind some macros for each structure that you want to use it in... YMMV