Home > Software engineering >  Can you "hop" between "linked classes" in C metaprogramming?
Can you "hop" between "linked classes" in C metaprogramming?

Time:07-06

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.

CodePudding user response:

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

CodePudding user response:

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>);

CodePudding user response:

You could make it a type trait:

#include <utility> // declval

template<class L>
struct Leaf {
    static L test(...); // match L's without Def, terminating match

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

    // find most specialized matching test() overload:
    using type = decltype( test(std::declval<L>()) );
};
#if __cplusplus >= 201703L
  template<class L> // helper variable template
  using Leaf_t = typename Leaf<L>::type;
#endif

Demo and template resolution @ cppinsights


Alternative version if test(...) doesn't appeal to you:

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;

    using type = decltype( test(std::declval<L>()) );
};
  • Related