2.2. Reflection: Reflection Interfaces - TheNitesWhoSay/RareCpp GitHub Wiki

RareTs

The RareTs ("Rare Type Support") namespace is available as a top level namespace after including reflect.h. RareTs provides interfaces for accessing information about reflected types, as well as other traits and functions for more general type introspection - expanding on capabilities you'd find in standard library headers like <type_traits>.

RareTs has several interfaces and traits for accessing information about reflected types. A collection of what is typically considered most useful is exposed through the RareTs::Reflect<T> object (where T is some reflected type), making it a good starting point in a search for some capability. However, all of Reflect<T>'s members are aliases for types/interfaces in RareTs, and direct use of those is more convenient once you know what you're looking for.

The four most important interfaces, and ergo, the interfaces we'll cover in this document (or in notes document) are:

  1. RareTs::Members<T> - access to member values and metadata
  2. RareTs::Member<T, I> - access to the Ith members value and metadata
  3. RareTs::Supers<T> - access to super-classes and super-class metadata
  4. RareTs::Notes<T> - access to class-level annotations

Member(s)

The "Member" and "Members" interfaces often work with each-other, but they're a bit different.

RareTs::Member<T, I> accesses details about a single reflected member using the type of the reflected object and some member index known at compile time.

RareTs::Members<T> provides methods for iterating, packing, as well as accessing members by (compile-time or runtime) name/index.

  • RareTs::Members<T>::forEach - iterates the members of T providing the member & value to the given callback
  • RareTs::Members<T>::at - given an index (which can be an index only known at runtime), provides the member & value to the given callback
  • RareTs::Members<T>::named - given a member name-string, provides the member & value to the given callback
  • RareTs::Members<T>::pack - provides a parameter pack of members to the given callback

forEach example Run

struct MyObj
{
    int a = 0;
    std::string b = "asdf";

    REFLECT(MyObj, a, b)
};

int main()
{
    MyObj myObj {};
    RareTs::Members<MyObj>::forEach(myObj, [&](auto member, auto & value) {
        std::cout << member.name << " = " << value << std::endl;
    });
}

The above demonstrates a simple usage of Members::forEach, it takes some reflected object type (in this case, "MyObj"), optionally you may provide an instance ("myObj") so as to be able to get instance values; then it takes a lambda (or other function/callable) which has a member argument and (optionally) a value argument; said lambda/callable will be invoked once for each member.

The member argument holds additional metadata about the member being visited, such as the members name, index, etc; member is of type RareTs::Member<T, I>. RareTs::Member<T, I> has no instance members, and it's not uncommon to need the type of it without the reference: so it is best to take "member" by value. Value on the other hand could be of any of the member types in your reflected object, so to avoid large copies and to enable modification of the value, it's usually best to have the "value" argument be a reference.

There are a couple variations to the forEach method, you can exclude the object instance Run , use a lambda that doesn't include "member" metadata Run , or add a filter Run. A table enumerating all of them is further down on this page.

at example Run

struct MyObj
{
    int a = 0;
    std::string b = "asdf";

    REFLECT(MyObj, a, b)
};

int main()
{
    MyObj myObj {};
    size_t memberIndex = 1; // Doesn't need to be known at compile-time!
    RareTs::Members<MyObj>::at(memberIndex, myObj, [&](auto member, auto & value) {
        std::cout << member.name << " = " << value;
    });
}

The "at" method is for visiting one particular member, specifically, "at" should be used for a member whose index you don't know at compile time, perhaps because you looked up the field index based on some user input. If you do know the member index (or member identifier) at compile time, you should be using RareTs::Member<T, I> Run (or the RareTs::MemberType<T> interface Run)

The "at" method has similar variations/overloads as "forEach" (enumerated below) that can be used depending on your needs.

named example Run

struct MyObj
{
    int a = 0;
    std::string b = "asdf";

    REFLECT(MyObj, a, b)
};

int main()
{
    MyObj myObj {};
    std::string_view memberName = "b"; // Doesn't need to be known at compile-time!
    RareTs::Members<MyObj>::named(memberName, myObj, [&](auto member, auto & value) {
        std::cout << member.name << " = " << value;
    });
}

The "named" method visits a particular member using the name string you provide, very similar in nature to the "at" method.

The "named" method has similar variations/overloads as "forEach" and "at" (enumerated below) that can be used depending on your needs.


forEach, at, and named variations

Note that there's a sibling interface to RareTs::Members called RareTs::Values, if you only want the values and have no need for member metadata, you can call forEach, at, or named on RareTs::Values.

RareTs::Members<MyObj>::forEach([&](auto member) { /** some code using only member metadata */ });
RareTs::Members<MyObj>::forEach([&](auto member, auto & value) { /** some code using static members & static values */ });
RareTs::Members<MyObj>::forEach(myObj, [&](auto member, auto & value) { /** some code using all members & values */ });
RareTs::Values<MyObj>::forEach([&](auto & value) { /** some code using static values */ });
RareTs::Values<MyObj>::forEach(myObj, [&](auto & value) { /** some code using all member values */ });

RareTs::Members<MyObj>::forEach<MyFilter>([&](auto member) { /** some code using only the filtered members metadata */ });
RareTs::Members<MyObj>::forEach<MyFilter>([&](auto member, auto & value) { /** some code using only filtered static members */ });
RareTs::Members<MyObj>::forEach<MyFilter>(myObj, [&](auto member, auto & value) { /** some code using filtered members & values */ });
RareTs::Values<MyObj>::forEach<MyFilter>([&](auto & value) { /** some code using filtered static values */ });
RareTs::Values<MyObj>::forEach<MyFilter>(myObj, [&](auto & value) { /** some code using filtered member values */ });
Scope Name Overloads func(member) func(value) func(member, value)
RareTs::Members<T>:: forEach (func) ✅*
(t, func)
at (index, func) ✅*
(index, t, func)
named (name, func) ✅*
(name, t, func)
RareTs::Values<T>:: forEach (func) ✅*
(t, func)
at (index, func) ✅*
(index, t, func)
named (name, func) ✅*
(name, t, func)
  • * only visits static members/values
  • t is an instance of the reflected type T, index is a memberIndex and name is a memberName
  • func is a lambda or other callable that takes one arg "member" or two args "member, value"
  • the above functions can all be filtered using template parameters <Filter, Args...>

pack example Run

class MyObj
{
    bool a = true;
    bool b = true;
    bool c = false;

public:
    REFLECT(MyObj, a, b, c)
};

int main()
{
    MyObj myObj{};
    RareTs::Values<MyObj>::pack(myObj, [&](auto & ... value) {
        bool andResult = true && (... && value);
        bool orResult = (... || value);
        (std::cout << ... << (value ? " true &&" : " false &&")) << " = " << (andResult ? "true" : "false") << std::endl;
        (std::cout << ... << (value ? " true ||" : " false ||")) << " = " << (orResult ? "true" : "false") << std::endl;
    });
}

Finally we have the "pack" methods, these are geared towards more advanced C++ users familiar with packs and pack expansion/fold expressions, use of packs can improve the compile and runtime performance of your code over iteration/indexed access and can on occasion be cleaner than the alternatives. The lambda/callable given to the pack function should accept a pack of members by value (if called on ::Members) or a pack of values by reference (if called on ::Values).

Scope Name Overloads
RareTs::Members<T>:: pack (func)
RareTs::Values<T>:: pack (t, func)
  • func is a lambda or function that takes a pack of members or values
  • the above functions can all be filtered using template paramters <Filter, Args...>

Supers

Supers are declared using class-level annotations on objects reflected (or proxy-reflected) using the REFLECT_NOTED macro.

When you declare supers you are declaring what the current type/the type you're annotating inherits from (be it singular or multiple-inheritance), ancestors further up the chain (if present) are declared on their own classes (or on proxies for said classes, as the case may be).

The RareTs::Supers<T> interface is the primary way you read declared supers and any notes attached to the super-class relationships. It has methods similar to those in the members interface.

  • RareTs::Supers<T>::forEach
  • RareTs::Supers<T>::forEachSuper
  • RareTs::Supers<T>::at
  • RareTs::Supers<T>::superAt

forEach example Run

struct Parent
{
    int a = 1;
    REFLECT(Parent, a)
};

NOTE(Child, RareTs::Super<Parent>) // Supers are defined in a class-level annotations like so
struct Child : Parent
{
    int b = 2;
    REFLECT_NOTED(Child, b) // Class-level annotations require the "REFLECT_NOTED" rather than the "REFLECT" macro
};

int main()
{
    Child child {};
    RareTs::Supers<Child>::forEach(child, [&](auto superInfo, auto & super) {
        using SuperType = typename decltype(superInfo)::type;
        std::cout << "Child has super: " << RareTs::toStr<SuperType>() << " " << super.a << std::endl;
    });
    return 0;
}

Of a very similar form to the members interface, the forEach method takes the type of the object you're examining "Child", optionally an instance of that type "child", then a lambda or other callable which accepts "superInfo" (metadata about the super-class/super-class relationship) by value and an optional "super" (an instance of the super class/the instance you provided casted to the super-class type) by reference.

As stated, using an instance is optional Run , you can also use the RareTs::Supers<T>::forEachSuper method to visit only the instances/not include any superInfo metadata Run (which includes the super type, index, and notes).


at example Run

struct Parent
{
    int a = 1;
    REFLECT(Parent, a)
};

NOTE(Child, RareTs::Super<Parent>) // Supers are defined in a class-level annotations like so
struct Child : Parent
{
    int b = 2;
    REFLECT_NOTED(Child, b) // Class-level annotations require the "REFLECT_NOTED" rather than the "REFLECT" macro
};

int main()
{
    Child child {};
    RareTs::Supers<Child>::at(0, child, [&](auto superInfo, auto & super) {
        using SuperType = typename decltype(superInfo)::type;
        std::cout << "Child has super at index 0: " << RareTs::toStr<SuperType>() << " " << super.a << std::endl;
    });
    return 0;
}

Much like the RareTs::Members<T>::at method, this visits the super at a particular index (which can be an index you only know at runtime), may or may not include an instance Run , and like RareTs::Supers<T>::forEach you can visit only the super instances with the RareTs::Members<T>::superAt method Run.

RareTs::Supers<T>::at should only be used when you don't know the index of the super you want to visit at compile time, if you do know the index, you should use RareTs::Supers<T>::SuperInfo<I> or RareTs::Supers<T>::SuperType<I> Run .


traversing inheritance trees Run

struct GrandparentA {};
struct GrandparentB {};

NOTE(ParentA, RareTs::Super<GrandparentA>)
struct ParentA : GrandparentA { REFLECT_NOTED(ParentA) };

NOTE(ParentB, RareTs::Super<GrandparentB>)
struct ParentB : GrandparentB { REFLECT_NOTED(ParentB) };

NOTE(Child, RareTs::Super<ParentA>, RareTs::Super<ParentB>)
struct Child : ParentA, ParentB { REFLECT_NOTED(Child) };

template <typename T, size_t Level = 0>
void printTree()
{
    if constexpr ( Level > 0 )
    {
        for ( size_t i=0; i<Level; ++i )
            std::cout << "  ";
    }

    std::cout << RareTs::toStr<T>() << std::endl;
    if constexpr ( RareTs::is_reflected_v<T> )
    {
        RareTs::Supers<T>::forEach([](auto superInfo) { // Note that this only loops over the (zero, one, or multiple) inherited classes immediately on type T
            printTree<typename decltype(superInfo)::type, Level+1>(); // Generally need to grab the type of a super and recurse on it to get to ancestors
        });
    }
}

int main()
{
    printTree<Child>();
    return 0;
}

Traversal of inheritance trees is best done using recursion, and when you need to access the members of some super-class, you simply need the type of that super-class (and maybe an instance), and you can then call something on RareTs::Members<T> with T being that super type.

Up Next...

  • Notes - teaches how to access reflected note information
⚠️ **GitHub.com Fallback** ⚠️