When talking about reflection, the 2 mistakes that are most common...
for (const auto& mbr : my_struct)
{
// but what is the type of mbr now, it changes for every member
// you cannot "loop" over things of different types.
}
But... While most programmers find 'for loop's a comfortable and familiar way of writing code it is in-fact a bit of an anti-pattern in modern C++. You should prefer algorithms and "visitation". Once you learn to give up on iteration, and prefer visitation (passing functions to algorithms), you find that the pattern I describe below is quite usable.
So what is the easy way... Given just three techniques you can roll your own reflection system in C++17 onwards in a hundred lines of code or so.
template<typename... Ts>
std::ostream& operator<<(std::ostream& os, std::tuple<Ts...> const& theTuple)
{
std::apply
(
[&os](Ts const&... tupleArgs)
{
os << '[';
std::size_t n{0};
((os << tupleArgs << (++n != sizeof...(Ts) ? ", " : "")), ...);
os << ']';
}, theTuple
);
return os;
}
Understand this code before reading on...
What you need a system that makes tuples from structures. Boost-PFR or Boost-Fusion are good at this, if you want a quick-start to experiment on.
The best way to access a member of a structure is using a pointer-to-member. See "Pointers to data members" at https://en.cppreference.com/w/cpp/language/pointer. The syntax is obscure, but this is a pre-C++11 feature and is a stable feature of C++.
You can make a static-member function that constructs a tuple-type for your structure. For example, the code below makes a tuple of member pointers for "Point", pointers to the "offset" of the members x & y. The member-pointers can be determined at compile-time, so this comes with a mostly zero-cost overhead. member-pointers also retain the type of the object they point to and are type-safe. (Every compiler I have used will not actually generate a tuple, just generate the code produced, making this a zero-overhead technique... I can't promise this but it normally is) Example struct...
struct Point
{
int x{ 0 };
int y{ 0 };
static consteval auto get_members() {
return std::make_tuple(
&Point::x,
&Point::y
);
}
};
You can now wrap all the nastiness up in simple wrapper functions. For example.
// usage visit(my_point, [](const auto& mbr) { std::cout << mbr; });
// my_point is an object of the type point which has a get_members function.
template <class RS, class Fn>
void visit(RS& obj, Fn&& fn)
{
const auto mbrs = RS::get_members();
const auto call_fn = [&](const auto&...mbr)
{
(fn(obj.*mbr.pointer), ...);
};
std::apply(call_fn, mbrs);
};
To use all you have to do is make a "get_members" function for every class/structure you wish to use reflection on.
I like to extend this pattern to add field names and to allow recursive visitation (when the visit function sees another structure that has a "get_members" function it visits each member of that too). C++20 also allows you to make a "concept" of visitable_object, which gives better errors, when you make a mistake. It is NOT much code and while it requires you to learn some obscure features of C++, it is in fact easier than adding meta-compilers for your code.