2.4. Reflection: Adaptive Structures - TheNitesWhoSay/RareCpp GitHub Wiki

Introduction

An adaptive structure is a structure generated through compile-time metaprogramming that contains one or more members sharing the names of members from a reflected class.

While one or more of the members of an adaptive structure share the name of the member from the reflected class, they do not (have-to) share the type, the code generating the adaptive structure can determine the type.

Adaptive structures make it possible to generate code such as "perfect" builders, change-listeners, testing white-boxers, object-field-mapping definers and so on.

[TODO: Animated gifs showing off the above]

WARNING: while it's simple to use pre-built adaptive structures like RareBuilder, understanding how adaptive structures work and how to create your own will most likely require advanced C++ metaprogramming knowledge, dive in at your own risk.

Adaptive member basis

The REFLECT macro, among other things, will create two templates per member "adapt_member" and "adapt_member_type" Run

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

    // Generated by REFLECT macro (in a nested structure)
    template <size_t MemberIndex, template <size_t MemberIdx> class>
    struct adapt_member;

    template <template <size_t MemberIndex> class T>
    struct adapt_member<0, T> {
        T<0> a;
    };
    template <template <size_t MemberIndex> class T>
    struct adapt_member<1, T> {
        T<1> b;
    };

    template <size_t MemberIndex, template <size_t MemberIdx> class>
    struct adapt_member_type;

    template <template <size_t MemberIndex> class T>
    struct adapt_member_type<0, T> {
        using a = T<0>;
    };
    template <template <size_t MemberIndex> class T>
    struct adapt_member_type<1, T> {
        using b = T<1>;
    };
    // End REFLECT macro
};

By creating a structure that inherits from MyObj::adapt_member or MyObj::adapt_member_type, you create a structure which will have members named the same as in MyObj, that is, members named "a" and "b". But it wouldn't be terribly useful if to have members named the same unless we could supply a type for those members, the mechanism used to supply a type is the template template parameter T. T is some template, which, given a member index, resolves to some complete type.

One simple example of using a template template parameter is as follows: Run

template <int I> using index_type = std::integral_constant<int, I>;

template <int I, template <int> class T> // T is template which will be completed by giving it a int
struct ValueGetter
{
    using CompleteType = T<I>; // We give T a int (I) to get a complete type (std::integral_constant<int, I>)
    int getValue() { return CompleteType::value; }
};

int main() { return ValueGetter<5, index_type>().getValue(); } // Returns 5

Using something like this with our "MyObj" adapters we get a structure whose members are named "a" and "b" and whose types are std::integral_constant<size_t, 0> and std::integral_constant<size_t, 1> respectively. Run

template <size_t I> using index_type = std::integral_constant<size_t, I>;

struct MyObjIndexTypes :
    MyObj::adapt_member_type<0, index_type>,
    MyObj::adapt_member_type<1, index_type>
{
    // ~same as if this structure had
    // using a = std::integral_constant<size_t, 0>;
    // using b = std::integral_constant<size_t, 1>;
};

struct MyObjIndexValues :
    MyObj::adapt_member<0, index_type>,
    MyObj::adapt_member<1, index_type>
{
    // ~same as if this structure had
    // std::integral_constant<size_t, 0> a;
    // std::integral_constant<size_t, 1> b;
};

int main() {
    std::cout << MyObjIndexTypes::a::value << std::endl; // Prints 0
    std::cout << MyObjIndexTypes::b::value << std::endl; // Prints 1
    MyObjIndexValues myObjIndexValues {};
    std::cout << decltype(myObjIndexValues.a)::value << std::endl; // Prints 0 (here decltype is just getting the type of member a on object myObjIndexValues)
    std::cout << decltype(myObjIndexValues.b)::value << std::endl; // Prints 1
}

Now let's let the adapt_member and adapt_member_type structures be generated by the REFLECT macro instead of us doing that manually (when relying on REFLECT, you access these via RareTs::Class::adapt_member and adapt_member_type, taking the template template param, then the object type, then the index). Run

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

    REFLECT(MyObj, a, b)
};

template <size_t I> using index_type = std::integral_constant<size_t, I>;

struct MyObjIndexTypes :
    RareTs::Class::adapt_member_type<index_type, MyObj, 0>,
    RareTs::Class::adapt_member_type<index_type, MyObj, 1>
{
    // ~same as if this structure had
    // using a = std::integral_constant<size_t, 0>;
    // using b = std::integral_constant<size_t, 1>;
};

struct MyObjIndexValues :
    RareTs::Class::adapt_member<index_type, MyObj, 0>,
    RareTs::Class::adapt_member<index_type, MyObj, 1>
{
    // ~same as if this structure had
    // std::integral_constant<size_t, 0> a;
    // std::integral_constant<size_t, 1> b;
};

int main() {
    std::cout << MyObjIndexTypes::a::value << std::endl; // Prints 0
    std::cout << MyObjIndexTypes::b::value << std::endl; // Prints 1
    MyObjIndexValues myObjIndexValues {};
    std::cout << decltype(myObjIndexValues.a)::value << std::endl; // Prints 0 (here decltype is just getting the type of member a on object myObjIndexValues)
    std::cout << decltype(myObjIndexValues.b)::value << std::endl; // Prints 1
}

Next, instead of just getting member indexes let's do something mildly more interesting and use the member types from MyObj as the types in our adaptive structures... RareTs::Member<T, Index>::type does that for us, but our template template parameter only has a size_t, so we need to wrap our template (what was the index_type alias before) in a structure that gives us that T: Run

template <typename T> struct wrap {
    template <size_t I> using type = typename RareTs::Member<T, I>::type;
};

struct MyObjTypes :
    RareTs::Class::adapt_member_type<wrap<MyObj>::template type, MyObj, 0>,
    RareTs::Class::adapt_member_type<wrap<MyObj>::template type, MyObj, 1>
{
    // ~same as if this structure had
    // using a = int;
    // using b = std::string;
};

struct MyObjValues :
    RareTs::Class::adapt_member<wrap<MyObj>::template type, MyObj, 0>,
    RareTs::Class::adapt_member<wrap<MyObj>::template type, MyObj, 1>
{
    // ~same as if this structure had
    // int a;
    // std::string b;
};

int main() {
    std::cout << RareTs::toStr<MyObjTypes::a>() << std::endl; // prints int
    std::cout << RareTs::toStr<MyObjTypes::b>() << std::endl; // prints ~std::string
    MyObjValues myObjValues {};
    std::cout << RareTs::toStr<decltype(myObjValues.a)>() << std::endl; // prints int
    std::cout << RareTs::toStr<decltype(myObjValues.b)>() << std::endl; // prints ~std::string
}

Now these are starting to look a little interesting, the first struct allows us to get the types of members in MyObj by their identifier, and the second struct is a type mirroring the contents of MyObj; but without much difficulty we can make these work for any types, not just MyObj, so let's turn these adaptive structures into templates taking the type of the object we wish to adapt, and the indexes of the members we wish to adapt, and then inherit from adapt_member/adapt_member_type for those types/indexes (this will involve some pack expansion). Run

template <typename T> struct wrap {
    template <size_t I> using type = typename RareTs::Member<T, I>::type;
};

template <typename T, size_t ... Is>
struct ObjTypes : RareTs::Class::adapt_member_type<wrap<T>::template type, T, Is>...
{
    // ~same as if this structure had
    // using a = int;
    // using b = std::string;
};

template <typename T, size_t ... Is>
struct ObjValues : RareTs::Class::adapt_member<wrap<T>::template type, T, Is>...
{
    // ~same as if this structure had
    // int a;
    // std::string b;
};

int main() {
    std::cout << RareTs::toStr<ObjTypes<MyObj, 0, 1>::a>() << std::endl; // prints int
    std::cout << RareTs::toStr<ObjTypes<MyObj, 0, 1>::b>() << std::endl; // prints ~std::string
    ObjValues<MyObj, 0, 1> myObjValues {};
    std::cout << RareTs::toStr<decltype(myObjValues.a)>() << std::endl; // prints int
    std::cout << RareTs::toStr<decltype(myObjValues.b)>() << std::endl; // prints ~std::string
}

One more step: while in our example MyObj has member indexes 0 and 1 that won't be the case for every object, so let's generate that index sequence... Run

template <size_t ... Is>
void useSequence(std::index_sequence<Is...>)
{
    (std::cout << ... << Is) << std::endl;
}

int main() {
    useSequence(std::make_index_sequence<2>()); // Passes an index sequence with indexes 0 and 1 to "useSequence", prints "01"
    useSequence(std::make_index_sequence<RareTs::Members<MyObj>::total>()); // Same as previous line, except using reflected member total
}

Applying such a sequence to our use case and wrapping up the return type in a nice alias: Run

template <typename T> struct wrap {
    template <size_t I> using type = typename RareTs::Member<T, I>::type;
};

template <typename T, size_t ... Is>
struct ObjTypes : RareTs::Class::adapt_member_type<wrap<T>::template type, T, Is>...
{
    // ~same as if this structure had
    // using a = int;
    // using b = std::string;
};

template <typename T, size_t ... Is>
struct ObjValues : RareTs::Class::adapt_member<wrap<T>::template type, T, Is>...
{
    // ~same as if this structure had
    // int a;
    // std::string b;
};

template <typename T, size_t ... Is>
ObjTypes<T, Is...> objTypes(std::index_sequence<Is...>); // We just need the return type, we don't need a function body

template <typename T, size_t... Is>
ObjValues<T, Is...> objValues(std::index_sequence<Is...>);

template <typename T>
using obj_types = decltype(objTypes<T>(std::make_index_sequence<RareTs::Members<T>::total>()));

template <typename T>
using obj_values = decltype(objValues<T>(std::make_index_sequence<RareTs::Members<T>::total>()));

int main() {
    std::cout << RareTs::toStr<typename obj_types<MyObj>::a>() << std::endl; // prints int
    std::cout << RareTs::toStr<typename obj_types<MyObj>::b>() << std::endl; // prints ~std::string
    obj_values<MyObj> myObjValues { {1}, {"qwerty"} };
    std::cout << myObjValues.a << std::endl; // prints 1
    std::cout << myObjValues.b << std::endl; // prints querty
}

And there we have two complete, if not terribly useful, adaptive structures, obj_types adapts member types such that we can get the types of the given structure by the member identifier, and the other adapts member instances essentially cloning the source structure. In the next section we'll make a considerably more useful but considerably more complicated adaptive structure: a builder.

Adaptive Builder

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

    REFLECT(MyObj, a, b)
};

The basic form we would want in a builder is something like...

builder<T>().build(); // should return T{}
builder<T>().a(1).build(); // should return ~{ T t{}; t.a = 1; return t; }
builder<T>().b("qwerty").build(); // should return ~{ T t{}; t.b = "qwerty"; return t; }
builder<T>().a(1).b("qwerty").build(); // should return ~{ T t{}; t.a = 1; t.b = "qwerty"; return t; }
builder<T>().b("qwerty").a(1).build(); // should return ~{ T t{}; t.b = "qwerty"; t.a = 1; return t; }

In a builder, members should be removed after we use them, such that...

builder<T>() // should return a structure with members {a, b, build()}, a and b should be callable objects accepting a single "const int &" or "const std::string &" respectively
builder<T>().a(1); // should return a structure with members {b, build()}, b should be a callable object accepting "const std::string &"
builder<T>().b("qwerty"); // should return a structure with members {a, build()}, a should be a callable object accepting "const int &"
builder<T>().a(1).b("qwerty"); // should return a structure with members {build()}

Since we adapt members using member indexes, we know we'll need an index sequence to represent the buildable members (non-static data members) of our reflected object. Let's start by creating an entry point which creates an index sequence (borrowing some code from above). Run

template <typename T, size_t ... Is>
auto builder(std::index_sequence<Is...>) // Takes the type we want to build, T, and an index sequence of buildable members
{
    throw std::exception(); // TODO
}

template <typename T>
auto builder()
{
    return builder(std::make_index_sequence<RareTs::Members<T>::total>()); // Makes an index sequence for all members
}

But we don't want all members, we want non-static data members, which is something we can get by applying a filter to a pack of members. Run

template <typename T, size_t ... Is>
auto builder(std::index_sequence<Is...>) // Takes the type we want to build, T, and an index sequence of buildable members
{
    throw std::exception(); // TODO
}

template <typename T>
auto builder()
{
    return RareTs::Members<T>::template pack<RareTs::Filter::IsInstanceData>([&](auto ... member) {
        return builder(std::index_sequence<decltype(member)::index...>{}); // Makes an index sequence for non-static data members
    });
}

Next we need our builder() method to return a structure that knows type T and our index sequence and contains at least a .build() method... Run

template <typename T, size_t ... Is>
struct Builder
{
    T build() { throw std::exception(); } // TODO
};

template <typename T, size_t ... Is>
auto builder(std::index_sequence<Is...>) // Takes the type we want to build, T, and an index sequence of buildable members
{
    return Builder<T, Is...>();
}

template <typename T>
auto builder()
{
    return RareTs::Members<T>::template pack<RareTs::Filter::IsInstanceData>([&](auto ... member) {
        return builder(std::index_sequence<decltype(member)::index...>{}); // Makes an index sequence for non-static data members
    });
}

The Builder class will also need to have members named the same as our reflected object, and as such, Builder needs to inherit from adapt_member, but we don't yet have an appropriate template we could pass as a template template param to Builder, so let's make one and adapt it. Run

template <size_t I>
struct MemberBuilder
{
};

template <typename T, size_t ... Is>
struct Builder : RareTs::Class::adapt_member<MemberBuilder, T, Is>...
{
    T build() { throw std::exception(); } // TODO
};

MemberBuilder will need to set the value of member at index "I" on object "T", so let's wrap the structure so we can inform it of type T, and create a reference member to a T Run

template <typename T>
struct MemberBuilder
{
    template <size_t I>
    struct type
    {
        T & t;
    };
};


template <typename T, size_t ... Is>
struct Builder : RareTs::Class::adapt_member<MemberBuilder<T>::template type, T, Is>...
{
    T build() { throw std::exception(); } // TODO
};

Now let's add the operator() overload which we'll invoke on MemberBuilders to set the value of the member, and make that T& private... Run

template <typename T>
struct MemberBuilder
{
    template <size_t I>
    class type
    {
        using Member = typename RareTs::Member<T, I>;
        T & t;
    public:
        constexpr auto operator() (const typename Member::type & value) {
            Member::value(t) = value;
        }
    };
};


template <typename T, size_t ... Is>
struct Builder : RareTs::Class::adapt_member<MemberBuilder<T>::template type, T, Is>...
{
    T build() { throw std::exception(); } // TODO
};

Next we need an actual instance of T and we need a constructor for MemberBuilder that gives it a T&, so let's add those in... Run

template <typename T>
struct MemberBuilder
{
    template <size_t I>
    class type
    {
        using Member = typename RareTs::Member<T, I>;
        T & t;
    public:
        constexpr auto operator() (const typename Member::type & value) {
            Member::value(t) = value;
        }

        constexpr type(T & t) : t(t) {}
    };
};

template <typename T, size_t ... Is>
class Builder : RareTs::Class::adapt_member<MemberBuilder<T>::template type, T, Is>...
{
    T t {};
public:
    T build() { return t; }
    constexpr Builder() : RareTs::Class::adapt_member<MemberBuilder<T>::template type, T, Is> {{t}}... {}
};

Now we need to consider what we're going to return from operator(), we're supposed to return a builder type, which might be our "Builder" class, except that we need the builder class to remove the member we just set from the set of remaining members. So the Builder class we're returning is going to be a different type than the Builder class we just had; in particular it's going to differ by the index sequence (the "Is" sequence) in that the index of the member we just used will be removed.

But if we're returning a different builder instance then having our T instance in builder getting copied is less than ideal.

So let's start by giving Builder a forward declaration and moving that T instance outside of Builder... Run

template <typename T, size_t ... Is> class Builder;

template <typename T>
struct MemberBuilder
{
    template <size_t I>
    class type
    {
        using Member = typename RareTs::Member<T, I>;
        T & t;
    public:
        constexpr auto operator() (const typename Member::type & value) {
            Member::value(t) = value;
        }

        constexpr type(T & t) : t(t) {}
    };
};

template <typename T, size_t ... Is>
class Builder : RareTs::Class::adapt_member<MemberBuilder<T>::template type, T, Is>...
{
    T & t;
public:
    T build() { return t; }
    constexpr Builder(T & t) : RareTs::Class::adapt_member<MemberBuilder<T>::template type, T, Is> {{t}}..., t(t) {}
};

template <typename T, size_t ... Is>
auto builder(std::index_sequence<Is...>) // Takes the type we want to build, T, and an index sequence of buildable members
{
    T t {};
    return Builder<T, Is...>(t);
}

Next let's give MemberBuilder the current index sequence as a template parameter so we can remove an index from it and return a builder based on that; the new index sequence we make will also require expansion so from operator() we can call a new member function that does that expansion and constructs a builder. Run

template <typename T, size_t ... Is> class Builder;

template <typename T, size_t ... Is>
struct MemberBuilder
{
    template <size_t I>
    class type
    {
        using Member = typename RareTs::Member<T, I>;
        T & t;
        template <size_t ... Js>
        constexpr auto remaining(std::index_sequence<Js...>) { return Builder<T, Js...>(t); }

    public:
        constexpr auto operator() (const typename Member::type & value) {
            Member::value(t) = value;
            return remaining(typename RareTs::remove_index<I, Is...>::type{});
        }

        constexpr type(T & t) : t(t) {}
    };
};

template <typename T, size_t ... Is>
class Builder : public RareTs::Class::adapt_member<MemberBuilder<T, Is...>::template type, T, Is>...
{
    T & t;
public:
    T build() { return t; }
    constexpr Builder(T & t) : RareTs::Class::adapt_member<MemberBuilder<T, Is...>::template type, T, Is> {{t}}..., t(t) {}
};

template <typename T, size_t ... Is>
auto builder(std::index_sequence<Is...>) // Takes the type we want to build, T, and an index sequence of buildable members
{
    T t {};
    return Builder<T, Is...>(t);
}

template <typename T>
auto builder()
{
    return RareTs::Members<T>::template pack<RareTs::Filter::IsInstanceData>([&](auto ... member) {
        return builder<T>(std::index_sequence<decltype(member)::index...>{}); // Makes an index sequence for non-static data members
    });
}

Quite a task; but now we have a functional builder usable with any reflected object, let's try it once! Run

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

    REFLECT(MyObj, a, b)
};

template <typename T, size_t ... Is> class Builder;

template <typename T, size_t ... Is>
struct MemberBuilder
{
    template <size_t I>
    class type
    {
        using Member = typename RareTs::Member<T, I>;
        T & t;
        template <size_t ... Js>
        constexpr auto remaining(std::index_sequence<Js...>) { return Builder<T, Js...>(t); }

    public:
        constexpr auto operator() (const typename Member::type & value) {
            Member::value(t) = value;
            return remaining(typename RareTs::remove_index<I, Is...>::type{});
        }

        constexpr type(T & t) : t(t) {}
    };
};

template <typename T, size_t ... Is>
class Builder : public RareTs::Class::adapt_member<MemberBuilder<T, Is...>::template type, T, Is>...
{
    T & t;
public:
    T build() { return t; }
    constexpr Builder(T & t) : RareTs::Class::adapt_member<MemberBuilder<T, Is...>::template type, T, Is> {{t}}..., t(t) {}
};

template <typename T, size_t ... Is>
auto builder(std::index_sequence<Is...>) // Takes the type we want to build, T, and an index sequence of buildable members
{
    T t {};
    return Builder<T, Is...>(t);
}

template <typename T>
auto builder()
{
    return RareTs::Members<T>::template pack<RareTs::Filter::IsInstanceData>([&](auto ... member) {
        return builder<T>(std::index_sequence<decltype(member)::index...>{}); // Makes an index sequence for non-static data members
    });
}

int main()
{
    auto myObj = builder<MyObj>().a(1).b("qwerty").build();
    std::cout << myObj.a << std::endl;
    std::cout << myObj.b << std::endl;
    return 0;
}

Of course for builders in particular you don't need to do this all yourself, you can use RareBuilder, but hopefully this walkthrough of how something like that is implemented is helpful in creating your own adaptive structures.

⚠️ **GitHub.com Fallback** ⚠️