Can you "hop" between "linked classes" in C++ metaprogramming?

Viewed 147

Suppose you have something like this:

template<class D>
class HasDef {
public:
     typedef D Def;
};

class A : public HasDef<class B> {};
class B : public HasDef<class C> {};
class C {};

So it is like a "metaprogramming linked list", with type links, via the included typedef Def. Now I want to make a template "Leaf" that, when applied to A, follows the links to yield C:

void f() {
     Leaf<A>::type v; // has type C
}

Is it even possible at all to do this? I've tried some methods with std::compare and similar, but none are valid code: everything seems to run into issues with either that C has no Def typedef, or else that the type Leaf<> itself is incomplete when the inner recursive "call" is made so it (or its internal type type) cannot be referenced.

FWIW, the reason I want this is for making a "hierarchical state machine" where that Def represents the default state for each state in the hierarchy, and something a bit more elaborate that this seems to provide a fairly neat and clean "user interface syntax" for it.

3 Answers

I don't really like f(...) in modern code, thus my version uses void_t from C++17:

#include <type_traits>

template<class D>
struct HasDef {

     typedef D Def;
};

struct A : HasDef<class B> {};
struct B : HasDef<class C> {};
struct C {};


template <typename T, typename=void>
struct DefPresent : std::false_type{};

template <typename T>
struct DefPresent<T, std::void_t<typename T::Def>> : std::true_type{};


template<typename T, bool deeper = DefPresent<T>::value>
struct Leaf
{
    using Type = typename Leaf<typename T::Def>::Type;
};

template<typename T>
struct Leaf<T, false >
{
    typedef T Type;
};



static_assert(std::is_same<typename Leaf<C>::Type, C>::value, "C");
static_assert(std::is_same<typename Leaf<B>::Type, C>::value, "B");
static_assert(std::is_same<typename Leaf<A>::Type, C>::value, "A");

https://godbolt.org/z/5h5rfe81o

EDIT: just for completenes, 2 C++20 variants utilizing concepts. Tested on GCC 10

#include <type_traits>
#include <concepts>

template<class D>
struct HasDef {
     typedef D Def;
};

struct A : HasDef<class B> {};
struct B : HasDef<class C> {};
struct C {};

template <typename T>
concept DefPresent = requires(T a)
{
    typename T::Def;
};

template<typename T>
struct Leaf
{
    using Type = T;
};


template<typename T>
    requires DefPresent<T>
struct Leaf<T>
{
    using Type = Leaf<typename T::Def>::Type;
};


static_assert(std::is_same_v<typename Leaf<C>::Type, C>, "C");
static_assert(std::is_same_v<typename Leaf<B>::Type, C>, "B");
static_assert(std::is_same_v<typename Leaf<A>::Type, C>, "A");


template<typename T>
struct Leaf2
{
    template <typename M>
    static M test(M&&);

    template <DefPresent M>
    static auto test(M&&) -> typename Leaf2<typename M::Def>::Type;

    using Type = decltype(test(std::declval<T>()));
};

static_assert(std::is_same<typename Leaf2<C>::Type, C>::value, "C");
static_assert(std::is_same<typename Leaf2<B>::Type, C>::value, "B");
static_assert(std::is_same<typename Leaf2<A>::Type, C>::value, "A");

https://godbolt.org/z/vcqEaPrja

You can implement this with SFINAE to separate types that have the typedef from ones that don't when trying to follow them to their leaf. I used the trick from this SO answer here.

The first implementation for Leaf follows the typedef recursively, the second one just defines the type itself as there is no typedef to follow.

Also note I changed your classes to struct for default-public inheritance. Also I changed the order of their definitions for this to compile.

Compiler explorer

#include <type_traits>

namespace detail
{
    template<typename T> struct contains_def {
        template<typename U> static char (&test(typename U::Def const*))[1];
        template<typename U> static char (&test(...))[2];
        static const bool value = (sizeof(test<T>(0)) == 1);
    };
    
    template<typename T, bool has = contains_def<T>::value>
    struct Leaf {
        using type = typename Leaf<typename T::Def>::type;
    };

    template<typename T>
    struct Leaf<T, false> {
        using type = T;
    };
}

template <typename T>
using Leaf = detail::Leaf<T>; // expose Leaf to the global scope

template <typename T>
using Leaf_t = typename Leaf<T>::type; // for convenience


template<typename T>
struct AddDef {
    using Def = T;
};


struct C {};
struct B : AddDef<C> {};
struct A : AddDef<B> {};

static_assert(std::is_same_v<Leaf_t<A>, C>);
static_assert(std::is_same_v<Leaf_t<B>, C>);
static_assert(std::is_same_v<Leaf_t<C>, C>);

You could make it a type trait:

#include <utility> // declval

template<class L>
struct Leaf {
    template<class M>
    static L test(const M&); // match L's without Def

    template<class M>        // match L's with Def
    static auto test(M&&) -> typename Leaf<typename M::Def>::type;

    // find matching test() overload, prefer test(M&&):
    using type = decltype( test(std::declval<L>()) );
};

template<class L> // bonus helper types
using Leaf_t = typename Leaf<L>::type;

Demo and template resolution @ cppinsights

Related