Mircea Ispas
Mircea Ispas

Reputation: 20790

Get base class for a type in class hierarchy

Is is possible to get base class type in a class hierarchy?

For example:

struct A{};
struct B{} : public A;
struct C{} : public B;

I want some template that will have typedef Base<T>::Type inside like this:

Base<A>::Type == A
Base<B>::Type == A
Base<C>::Type == A

Is this possible? What about the case when I have multiple inheritance?

Upvotes: 16

Views: 21794

Answers (5)

linkhyrule5
linkhyrule5

Reputation: 918

@HolyBlackCat's answer is brilliant, but opaque. Here's my attempt at explaining it.

Working through the program step by step:

  • Our first trick: friend functions in classes can be declared and defined from within a class. This essentially adds a non-member function to the class's scope.

    • Subtrick: If we define our friend function in a class template, we have access to the specific template parameters used to define that specialization, and we can put those parameters in places the compiler -- and other templates -- can see.

      constexpr auto adl_ViewBase() {} //Dummy ADL target
      
      template <typename T>
      struct tag
      {
          using type = T;
      }
      
      namespace impl {
          template <typename D, typename B>
          struct BaseWriter
          {
              friend constexpr auto adl_ViewBase(D) {return tag<B>{};}
          };
      }
      

      Now, by putting an instance of BaseWriter into the base of D we want to detect, we can make the compiler add a corresponding overload of adl_ViewBase to the class scope that encodes the type of the base in the return type of the function.

    • Since it's not actually a member function, we can generically refer to adl_ViewBase and force ADL to create a pool of candidates consisting precisely of functions of our choice.

    • If we were to map this template-level meta-level code down to familiar object-level C++, this step would essentially define a class we might call an "unlinked list", to which we can append new elements. We don't have a way of actually accessing it yet, though.

  • Second trick: The above code will run into problems if we actually try to build too far on it, due to a subtlety in the way templates on classes and friend functions interact. In order that a specific friend function has access rights to a specific specialization of the class template, every friend function defined within a templated class is by default distinguished despite being implicitly inline, so if they have the same name and call signature they will be considered in violation of ODR. We can restore the 'normal' behavior by providing a forward declaration for each specific template-specialized function signature, which is easiest to achieve by providing the declaration in a template with similar template parameters.

        //...
        template <typename D, std::size_t I>
        struct BaseViewer {
            friend constexpr auto viewBase(BaseViewer<D, I>);
        };
    
        template <typename D, std::size_t I, typename B>
        struct BaseWriter
        {
            friend constexpr auto viewBase(BaseViewer<D, I>) {return tag<B>{};}
        };
    
    • We shouldn't complain, though, because in the process, we'll create a simple way of actually getting at the information we've extracted later. Assuming we can (a) find some way of passing unique values of I into the various BaseWriters and (b) keep track of the total number of valid values, we can systematically retrieve all the functions (and thus all the type tags) later by just querying the return types of viewBase for every valid I.
    • In other words, the "unlinked list" of the previous step has been been organized into a sort of primitive "map" in the internal workings of the compiler, with the numbered tagging struct BaseViewer acting as the keys that allow us to view a specific indexed base. (Thus the name.) Like a map, and unlike an array or list, we'll have to handle creating unique keys for our data ourselves. But that's a bit later. For now...
  • Third trick: since handling friend functions is part of specializing the template, the template instance doesn't necessarily need to be actually constructed to populate its part of the array. Then, we can exploit a certain feature of the C++ specification regarding which possible specializations a compiler must actually generate code for. Quoth cppreference.com,

    When code refers to a function in context that requires the function definition to exist, or if the existence of the definition affects the semantics of the program (since C++11), and this particular function has not been explicitly instantiated, implicit instantiation occurs. The list of template arguments does not have to be supplied if it can be deduced from context.

    The existence of a definition of function is considered to affect the semantics of the program if the function is needed for constant evaluation by an expression, even if constant evaluation of the expression is not required or if constant expression evaluation does not use the definition.

    (emphasis mine). In particular, non-type template parameters are always considered "manifestly constant-evaluated expressions" by the standard, so including a particular specialization of BaseWriter into a template parameter is enough to force the compiler to generate code for it (thus producing the friend function, thus populating our "map") even if the template ends up being unused.

    • We add this to a standard application of SFINAE to ensure that the function template we create never gets used and thus never makes it into runtime evaluation. We can now introduce our foundation into a CRTP base that our actual bases can inherit from to trigger the structures we've constructed above:

      #include <typeindex>
      #include <utility>
      
      template <typename T>
      struct Base
      {
          template <
              typename D,
              std::enable_if_t<std::is_base_of_v<T, D>, std::nullptr_t> = nullptr,
              typename impl::BaseWriter<D, /*???*/, T>::nonExistent = nullptr
          >
          friend constexpr void registerBases(void *) {}
      };
      
      //-----------------------
      
      //Sample object hierarchy for testing
      struct A : Base<A> {};
      struct B : Base<B>, A {};
      struct C : Base<C> {};
      struct D : Base<D>, B, C {};
      

      Here, T represents one of our base types, which will itself inherit from Base in a CRTP way. D is the derived class we're querying. Just like with normal && and ||, template substitution short-circuits if it encounters a substitution failure, so the compiler halts its substitution attempt if D does not derive T. Only if the enable_if passes does it try and instantiate the last parameter, which causes the compiler to append T into the array we're building. nullptr is used as a generic unit type for ignored arguments.

    • Since we've again declared and defined a friend member within our class template, every time we instantiate a class CRTP-derived from Base the compiler will automatically add a corresponding registerBases function template to the enclosing scope.

    • Of course, as the name implies, BaseWriter doesn't actually have a member called nonExistent for any values of T, D, so substitution will always fail. So there will be no code in the final runtime corresponding directly to registerBases.

  • Last trick before we tie everything up: ADL lookup. When we call a function by name, ADL has to first build a list of all possible definitions (overloads) of that name. If it encounters a function template by that name, it has to attempt to instantiate that template before it can know whether that definition should be added to the pool of candidates. This means that just considering a template is enough to activate all the machinery we have above. All we have to do is add an ADL dummy target somewhere that the compiler can fall back on once it realizes that none of the templates we've given it actually pass substitution. We change the name to adl_RegisterBases to document our intent, and put everything we've got together:

#include <typeindex>
#include <utility>

template <typename T>
struct tag
{
    using type = T;
};
    
namespace impl {
    // Don't need an ADL dummy for adl_ViewBase since we
    // only ever call it from inside impl::

    template <typename D, std::size_t I>
    struct BaseViewer {
        friend constexpr auto adl_ViewBase(BaseViewer);
    };

    template <typename D, std::size_t I, typename B>
    struct BaseWriter
    {
        friend constexpr auto adl_ViewBase(BaseViewer<D, I>) {return tag<B>{};}
    };
}

// Do need an ADL dummy for adl_RegisterBases somewhere
// SFINAE disables all the 'active' definitions, and the compiler needs
// to find *something* or it's a hard error, not SFINAE
template <typename T>
constexpr void adl_RegisterBases(void *) {} 

template <typename T>
struct Base
{
    template <
        typename D,
        std::enable_if_t<std::is_base_of_v<T, D>, std::nullptr_t> = nullptr,
        typename impl::BaseWriter<D, /*???*/, T>::nonExistent = nullptr
    >
    friend constexpr void adl_RegisterBases(void *) {}
};
//-----------------------

//Sample object hierarchy for testing
struct A : Base<A> {};
struct B : Base<B>, A {};
struct C : Base<C> {};
struct D : Base<D>, B, C {};

int main () {
    //ADL hint: by casting to (A *), we tell the compiler to check 
    //scopes related to A for possible overloads of adl_RegisterBases
    //Without the cast, the compiler just immediately instantiates our
    //dummy without actually checking relevant templates.
    //Casting nullptr to (A*) provides the hint without constructing A
    adl_RegisterBases<A>((A *)nullptr);
}

Now, all we have to do is figure out that bit we left for last: assigning different numbers to each base in a systematic way we can reproduce at the end. The easiest way to do that is to somehow keep track of the size the "map" for each target (derived class), and assign each new base the latest size of the map as its index.

So, we need something with a base case and general structure that looks like...

template<typename D, typename Next, std::size_t I = 0> 
struct NumBases : std::integral_constant<std::size_t, I> {};

A specific NumBases specification can only have a single ::value, which is a static property that can't be changed over the course of compilation; this is an issue if we intend to use the type template NumBases to keep track of the growing length of our map. So, instead we build NumBases as representing a particular basetype-index pair <Next, I> in itself, and attach it to a particular BaseViewer by passing its ::value to BaseWriter in a specialization of Base:

template <typename T>
struct Base
{
    template <
        typename D,
        std::enable_if_t<std::is_base_of_v<T, D>, std::nullptr_t> = nullptr,
        typename impl::BaseWriter<D, NumBases<D, T>::value, T>::nonExistent = nullptr
    >
    friend constexpr void adl_RegisterBases(void *) {}
};

At this point, enough of our code works and compiles that we can start following along with C++ Insights.

Note that BaseWriter has generated a specialization BaseWriter<A,0,A>, and NumBases a specialization NumBases<A, A, 0>. Specializing BaseWriter<A,0,A> has in turn generated BaseViewer<A, 0> and an overload of viewBase that accepts BaseViewer<A, 0> specifically. We can exploit this with our old friend SFINAE: we can make a specialization of NumBases that, being a specialization, will always be checked first, but only passes substitution if a corresponding BaseViewer with the right index value I (and ignoring Next) has already been specialized:

struct NumBases<D, Next, I, decltype(adl_ViewBase(BaseViewer<D, I>{})> : ...

If we recursively define NumBases in terms of itself, but with I replaced with I+1, then the recursion will terminate when the specialization stops applying -- that is, when the compiler runs out of existing BaseViewers with appropriate indices. As a side effect, by the same mechanism that populates our "map" in the first place, consideration by ADL will also instantiate BaseViewer<D, I+1>, incrementing the counter and preparing the stage for the next call to NumBases{}.

For clarity's sake, we move the SFINAE logic into its own template type parameter, so NumBases is now defined as

template<typename D, typename Next, std::size_t I = 0, typename = void>
struct NumBases : std::integral_constant<std::size_t, I> {};

template<typename D, typename Next, std::size_t I>
struct NumBases<D, Next, I, decltype(adl_ViewBase(BaseViewer<D, I>{}), void())
    : std::integral_constant<std::size_t, NumBases<D, Next, I+1, void>::value> {};

And that's the hard parts done: we can confirm with C++ Insights that NumBases does its job, giving us the keys (for the test struct named D) BaseViewer<D, 0> through BaseViewer<D, 3> and BaseWriter<D, 0, A> through BaseWriter<D, 3, D>. We use std::index_sequence and parameter pack expansion to enumerate the entries of our "map", package it in a type_list:

template <typename ...P>
struct type_list
{
    inline static constexpr std::size_t size = sizeof...(P);
};

namespace impl {
    //...
    template <typename T, typename I>
    struct BaseListLow {};
    
    template <typename T, std::size_t ...I>
    struct BaseListLow<T, std::index_sequence<I...>>
    {
        static constexpr type_list<decltype(adl_ViewBase(BaseViewer<T, I>{}))...> helper() {}
        using type = decltype(helper());
    };

...clean up our code some (packing up the direct call to adl_RegisterBases, with its very specific <T>((T *)nullptr) syntax, into its own function; making a helper struct impl::RegisterBases that hides some implementation details from the point of call in Base proper -- this means we have to move adl_RegisterBases itself into impl so the call from RegisterBases can find it):

    template <typename D, typename B>
    struct BaseInserter : BaseWriter<D, NumBases<D, B>::value, B> {};

    template <typename T>
    struct RegisterBases : decltype(adl_RegisterBases<T>((T *)nullptr), tag<void>())
    {};

    template <typename T>
    struct BaseList : BaseListLow<T, 
    std::make_index_sequence<(impl::RegisterBases<T>{}, NumBases<T, 
    void>::value)>> {};
} //namespace impl

template <typename T>
using base_list = typename impl::BaseList<T>::type;

template <typename T>
struct Base
{
    template <
        typename D,
        std::enable_if_t<std::is_base_of_v<T, D>, std::nullptr_t> = nullptr,
        typename impl::BaseInserter<D, T>::nonExistent = nullptr
    >
    friend constexpr void adl_RegisterBases(void *) {}
};

... and silence a few warnings from GCC about our non-templated friend function (probably warning us about exactly the weird inline-defeating behavior we had to deal with in step two).

And then the code runs. To use it, use CRTP to include the boilerplate adl_RegisterBases friend template in all the bases you want to detect, and then for whatever type T, just get the type of base_list<T>.

Run on gcc.godbolt.org

#include <iostream>
#include <typeindex>
#include <utility>

template <typename T> struct tag {
    using type = T;
};

template <typename ...P>
struct type_list
{
    inline static constexpr std::size_t size = sizeof...(P);
};

namespace impl {
    // Don't need an ADL dummy for adl_ViewBase since we
    // only ever call it from inside impl::

    template <typename D, std::size_t I> struct BaseViewer {
        friend constexpr auto adl_ViewBase(BaseViewer);
    };

    template <typename D, std::size_t I, typename B> struct BaseWriter {
        friend constexpr auto adl_ViewBase(BaseViewer<D, I>) { return tag<B>{}; }
    };

    template <typename D, typename Next, std::size_t I = 0, typename = void>
    struct NumBases : std::integral_constant<std::size_t, I> {};

    template <typename D, typename Next, std::size_t I>
    struct NumBases<D, Next, I, decltype(adl_ViewBase(BaseViewer<D, I>{}), void())>
        : std::integral_constant<std::size_t, NumBases<D, Next, I+1, void>::value> {};

    template <typename D, typename B>
    struct BaseInserter : BaseWriter<D, NumBases<D, B>::value, B> {};

    // Do need an ADL dummy for adl_RegisterBases somewhere
    // SFINAE disables all the 'active' definitions, and the compiler needs
    // to find *something* or it's a hard error, not SFINAE
    template <typename T>
    constexpr void adl_RegisterBases(void *) {}

    template <typename T>
    struct RegisterBases : decltype(adl_RegisterBases<T>((T *)nullptr), tag<void>())
    {};

    template <typename T, typename I>
    struct BaseListLow {};

    template <typename T, std::size_t ...I>
    struct BaseListLow<T, std::index_sequence<I...>>
    {
        static constexpr type_list<decltype(adl_ViewBase(BaseViewer<T, I>{}))...> helper() {}
        using type = decltype(helper());
    };

    template <typename T>
    struct BaseList : BaseListLow<T, std::make_index_sequence<(impl::RegisterBases<T>{}, NumBases<T, void>::value)>> {};
} //namespace impl

template <typename T>
using base_list = typename impl::BaseList<T>::type;

template <typename T>
struct Base
{
    template <
        typename D,
        std::enable_if_t<std::is_base_of_v<T, D>, std::nullptr_t> = nullptr,
        typename impl::BaseInserter<D, T>::nonExistent = nullptr
    >
    friend constexpr void adl_RegisterBases(void *) {}
};
//-----------------------

//Sample object hierarchy for testing
struct A : Base<A> {};
struct B : Base<B>, A {};
struct C : Base<C> {};
struct D : Base<D>, B, C {};

template <typename T>
void printType()
{
    #ifndef _MSC_VER
    std::cout << __PRETTY_FUNCTION__ << '\n';
    #else
    std::cout << __FUNCSIG__ << '\n';
    #endif
};

int main()
{
    static_assert( base_list<D>::size == 4 );
    printType<base_list<D>>(); // typeList<tag<A>, tag<B>, tag<C>, tag<D>>, order may vary
}

Upvotes: 1

HolyBlackCat
HolyBlackCat

Reputation: 96699

With certain limitations, it's possible!

  • Each base that needs to be detectable in this manner has to inherit from a certain CRTP base. (Or, possibly, contain some kind of macro.)

  • You get a list of all parents, including indirect ones.

Run on gcc.godbolt.org

#include <cstddef>
#include <iostream>
#include <typeindex>
#include <utility>

template <typename T>
struct tag
{
    using type = T;
};

template <typename ...P>
struct type_list
{
    inline static constexpr std::size_t size = sizeof...(P);
};

namespace impl
{
    constexpr void adl_ViewBase() {} // A dummy ADL target.

    template <typename D, std::size_t I>
    struct BaseViewer
    {
        #if defined(__GNUC__) && !defined(__clang__)
        #pragma GCC diagnostic push
        #pragma GCC diagnostic ignored "-Wnon-template-friend"
        #endif
        friend constexpr auto adl_ViewBase(BaseViewer);
        #if defined(__GNUC__) && !defined(__clang__)
        #pragma GCC diagnostic pop
        #endif
    };

    template <typename D, std::size_t I, typename B>
    struct BaseWriter
    {
        friend constexpr auto adl_ViewBase(BaseViewer<D, I>) {return tag<B>{};}
    };

    template <typename D, typename Unique, std::size_t I = 0, typename = void>
    struct NumBases : std::integral_constant<std::size_t, I> {};

    template <typename D, typename Unique, std::size_t I>
    struct NumBases<D, Unique, I, decltype(adl_ViewBase(BaseViewer<D, I>{}), void())> : std::integral_constant<std::size_t, NumBases<D, Unique, I+1, void>::value> {};

    template <typename D, typename B>
    struct BaseInserter : BaseWriter<D, NumBases<D, B>::value, B> {};

    template <typename T>
    constexpr void adl_RegisterBases(void *) {} // A dummy ADL target.

    template <typename T>
    struct RegisterBases : decltype(adl_RegisterBases<T>((T *)nullptr), tag<void>())
    {};

    template <typename T, typename I>
    struct BaseListLow {};

    template <typename T, std::size_t ...I>
    struct BaseListLow<T, std::index_sequence<I...>>
    {
        static constexpr type_list<decltype(adl_ViewBase(BaseViewer<T, I>{}))...> helper() {}
        using type = decltype(helper());
    };

    template <typename T>
    struct BaseList : BaseListLow<T, std::make_index_sequence<(impl::RegisterBases<T>{}, NumBases<T, void>::value)>> {};
}

template <typename T>
using base_list = typename impl::BaseList<T>::type;

template <typename T>
struct Base
{
    template <
        typename D,
        std::enable_if_t<std::is_base_of_v<T, D>, std::nullptr_t> = nullptr,
        typename impl::BaseInserter<D, T>::nonExistent = nullptr
    >
    friend constexpr void adl_RegisterBases(void *) {}
};


struct A : Base<A> {};
struct B : Base<B>, A {};
struct C : Base<C> {};
struct D : Base<D>, B, C {};

template <typename T>
void printType()
{
    #ifndef _MSC_VER
    std::cout << __PRETTY_FUNCTION__ << '\n';
    #else
    std::cout << __FUNCSIG__ << '\n';
    #endif
};

int main()
{
    static_assert( base_list<D>::size == 4 );
    printType<base_list<D>>(); // typeList<tag<A>, tag<B>, tag<C>, tag<D>>, order may vary
}

Here's what's going on:

  • You use stateful template metaprogramming to create a list of types, which you can append types to by instantiating a specific template.
  • Using a CRTP base, you add a friend function to each class that needs to be detectable in this manner. Those functions are made uncallable with SFINAE, but merely considering them during overload resolution instantiates the template that appends the corresponding base class to the list.
  • You invoke this overloaded function with ADL, and since overloads from all bases are considered and are attempted to be instantiated, you get a list of bases.

Upvotes: 4

Andrew Tomazos
Andrew Tomazos

Reputation: 68698

This might be a nice way to do it, depending on your use case. Declare a typedef of the base class named base in the base class itself.

Then derived classes X will inherit it as the typename X::base.

So B::base is A, and C::base is A.

struct A
{
    typedef A base;
};

struct B : A {};
struct C : B {};

template<class X>
void f()
{
    typename X::base x;
}

int main()
{
    f<B>();
    f<C>();
}

Upvotes: 10

masoud
masoud

Reputation: 56519

I think std::is_base_of can help you

#include <type_traits>

std::is_base_of<B, D>()

If D is derived from B or if both are the same non-union class, provides the member constant value equal to true. Otherwise value is false.

You can use it to check if a class is base class of another or not :

std::is_base_of<A, A>()   // Base<A>::Type == A

std::is_base_of<A, B>()   // Base<B>::Type == A

std::is_base_of<A, C>()   // Base<C>::Type == A

Upvotes: 9

Kerrek SB
Kerrek SB

Reputation: 477408

Classes in C++ can have more than one base class, so there's no sense in having a "get me the base" trait.

However, the TR2 additions include new compiler-supported traits std::tr2::bases and std::tr2::direct_bases, which returns an opaque type list of base classes.

I'm not sure whether this will make it into C++14, or whether it'll be released independently, but GCC already seems to support this.

Upvotes: 30

Related Questions