Reflection in C++14
The task is simple. How to write a template function that takes a class as argument and performs some operation on all its member variables, without requiring the class to contain extra code to get it working? This is extremely useful for serialisation. C# and languages where classes are implemented as hashtables (Python, JavaScript) support it, but C++ needs to do it at compile time, which is not available until Reflection TS (hopefully) lands in C++23.
There is a trick that allows to do it, using functionality was not supposed to exist in C++. I haven’t invented it, I have seen mentions of something like it on reddit and it took me quite an effort to google it. To be more exact, it was like the third answer on Stack Overflow on the fifth result of the tenth query I tried after seeing replies that it can’t be done. The link led to a github repository with several unrelated header-only libraries using very inventive tricks. Its importance was never mentioned and everything was badly documented. It felt like discovering a forgotten tome of forgotten powerful magic in a video game.
The basic idea
It can be done in runtime by attept to aggregate-initialise it from an array of objects with generic conversion operators defined.
// Class that can be converted to anything template <size_t N> struct ObjectInspector { template <typename Inspected>; operator Inspected() { return Inspected{}; } }; // Function that gets all elenents' conversion operators called template <typename T, size_t... indexes> void inspect(std::index_sequence<indexes...>) { T instance{ ObjectInspector<indexes>{}... }; }
If the following is called with the right argument to std::make_index_sequence
, the class will be created while calling methods templated to the right member type. They belong to classes that are templated to the index of member they are initialising and can be written to be initialised with a pointer to a vector where they can save information (including functors) that can work with the specific types.
The number of arguments can be determined through SFINAE, because aggregate initialisation can be called with less arguments than members, but not more.
This works on classes that can be aggregate-initialised, which basically requires them to contain no constructors, private or protected members, only public ineritance and no polymorphism. This is usually enough for classes we want to investigate this way. For other classes, it will investigate the constructor, which can be useful for dependency injection.
// The recursive break - selected when there are too many arguments // to construct the class // The number of elements will be one element more, // so it can return the actual size template <typename T, size_t... indexes> constexpr size_t getMemberCount(...) { // Less specific than an argument of concrete type return sizeof...(indexes) - 1; } // Tries to construct the class in SFINAE template <typename T, size_t... indexes, decltype( T {ObjectInspector<indexes>()...} )* = nullptr> constexpr size_t getMemberCount(void*) { // The argument is there only to make it more specific // If it's constructible, try to construct it with one more element return getMemberCount<T, indexes..., sizeof...(indexes)>(nullptr); }
This seems impossible to do at compile time, because the conversion operator’s return value is already used and releasing additional information has to be done through a side effect, which is against the principle of functional programming, which is the paradigm of C++ template metaprogramming. However, the repository demonstrates that it actually can be done.
Metaprogramming with side effects
This feature is documented as Defect Report 2118. It was found by the repository’s author. Here’s its basic usage:
// Abuse friend declaration to create a class that // defines the function when instantiated template <int num> struct Storage { friend int unstore() { return num; } }; // Friend functions need to be forward declared if they don't use // the class they're defined in as an argument int unstore(); Storage<11> instance; // This causes unstore() to be defined with return value 11 // It can be pretty much anywhere, as long as it's actually compiled // Yet elsewhere int done = unstore(); // will get 11
This shows linker errors if there are more instances of Storage
with different template arguments or none at all, so it’s not like a bad global variable that can be overwritten anywhere. GCC will show a warning, because defining non-template functions inside class templates usually prevents them from being instantiated more than once.
Because the above can store only a single integer value, it’s not very useful. But it can be used to store an array:
// Every index must be a unique type template <int N> struct Index { // Index needs to be used as an argument, // so it will be found if forward-declared here friend constexpr int access(Index<N>); }; // Storage now accepts indexes as arguments template <int index, int num> struct Storage { friend constexpr int access(Index<index>) { return num; } }; // elsewhere Storage<0, 12> instance; // ... int atIndex0 = access(Index<0>()); // will get 12
However, we’d like it to retrieve a type, not a value. The idea is not complicated, the return type can be auto
and the type returned may depend on a template argument:
template <int N> struct Index { friend constexpr auto access(Index<N>); }; // Storage now accepts indexes as arguments template <int index, typename T> struct Storage { friend constexpr auto access(Index<index>) { return T{}; } }; // placing this properly is surprisingly difficult Storage<0, float> instance; // ... decltype(access(Index<0>())) value; // Will be of type float
However, there’s a catch. The type of the return value isn’t known at the time of forward-declaration and must be known when the code that needs to know it is compiled, so the class template must be somehow guaranteed to be instantiated before the value is needed. This is hard to guarantee, because it’s done on the same compilation step. If it’s not instantiated, it shows a function 'access' with deduced return type cannot be used before it is defined
error.
This appears to be compiler-dependent. When investigating it using godbolt, the repository’s author’s code worked on GCC and Clang 7. It was also reported to work on MSVC. I tried to write it differently, and got it working on Clang 8 and Clang 9, but not GCC, Clang 7 or Clang 10. This path seems to be compiler-dependent, so it’s better not to rely on it.
However, there’s a way around. The access
function can do all of the code that depends on the member type, with a return value that does not depend on the member type. This is somewhat more boilerplate-prone. Furthermore, there is an extra problem – determining the offset of the member. Fortunately, the offset can be calculated, because they are always as closely packed as possible without placing any primitive type at unaligned location (this can be somewhat customised and we must assume it wasn’t).
Now, an actual reflection
A convenient example is a function that prints objects.
#include <iostream> #include <utility> // Forward declares for ADL template<typename T, int N> struct ObjectGetter { friend void processMember(ObjectGetter<T, N>, T*, size_t); friend constexpr int memberSize(ObjectGetter<T, N>); }; // The class that adds implementations according to its parametres template<typename T, int N, typename Stored> struct ObjectDataStorage { friend void processMember(ObjectGetter<T, N>, T* instance, size_t offset) { std::cout << N << ": " << *reinterpret_cast<Stored*>(reinterpret_cast<uint8_t*>(instance) + offset) << std::endl; }; friend constexpr int memberSize(ObjectGetter<T, N>) { return sizeof(Stored); } }; // The class whose conversions cause instantiations of ObjectDataStorage template<typename T, int N> struct ObjectInspector { template <typename Inspected, std::enable_if_t<sizeof(ObjectDataStorage<T, N, Inspected>) != -1>* = nullptr> operator Inspected() { return Inspected{}; } }; // Partial template specialisation for recursively finding member count template <typename T, typename sfinae, size_t... indexes> struct MemberCounter { constexpr static size_t get() { return sizeof...(indexes) - 1; } }; template <typename T, size_t... indexes> struct MemberCounter<T, decltype( T {ObjectInspector<T, indexes>()...} )*, indexes...> { constexpr static size_t get() { return MemberCounter<T, T*, indexes..., sizeof...(indexes)>::get(); } }; // Calculation of padding, assuming all composite types contain word-sized // members. True for std::string, smart pointers and all STL containers (NOT // std::array). Should use something specialised for all supported types in // the final version template <size_t previous, size_t size> constexpr size_t padded() { constexpr int wordSize = sizeof(void*); return (size==1 || size==2 || size==4 || (size==8 && wordSize==8)) ? ((previous + size) % size == 0 ? previous : previous + size - (previous + size) % size) : ((previous + size) % wordSize == 0 ? previous : previous + wordSize - (previous + size) % wordSize); } // Iteration through all elements, the first overload stops the recursion template <typename T, size_t offset> void goThroughElements(T* instance, std::index_sequence<>) { } template <typename T, size_t offset, size_t index, size_t... otherIndexes> void goThroughElements(T* instance, std::index_sequence<index, otherIndexes...>) { constexpr size_t size = memberSize(ObjectGetter<T, index>{}); constexpr size_t paddedOffset = padded<offset, size>(); processMember(ObjectGetter<T, index>{}, instance, paddedOffset); goThroughElements<T, paddedOffset + size>(instance, std::index_sequence<otherIndexes...>{}); } template <typename T> void iterateObject(T& instance) { goThroughElements<T, 0>(&instance, std::make_index_sequence<MemberCounter<T, T*>::get()>()); } // Usage #include <string> struct Mystery { int a = 3; std::string b = "We don't need C++17!"; float c = 4.5; bool d = true; void* e = nullptr; short int f = 13; double g = 14.34; }; int main() { std::cout << "Members: "; std::cout << MemberCounter<Mystery, Mystery*>::get() << std::endl; Mystery investigated; iterateObject(investigated); }
This gives the output:
Members: 7 0: 3 1: We don't need C++17! 2: 4.5 3: 1 4: 0 5: 13 6: 14.34
With some knowledge of metaprogramming, it can be altered to do all kinds of other purposes.