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.