c++ 获取类层次结构中类型的基类

q35jwt9p  于 2023-08-09  发布在  其他
关注(0)|答案(5)|浏览(160)

是否可以在类层次结构中获取基类类型?
举例来说:

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

字符串
我想要一些模板,将有typedef Base<T>::Type内像这样:

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


这可能吗?如果我有多重继承呢?

fnx2tebb

fnx2tebb1#

C中的类可以有多个基类,所以没有必要使用“get me the base”trait。
然而,TR2增加了新的编译器支持的trait std::tr2::basesstd::tr2::direct_bases,它们返回基类的不透明类型列表。
我不确定这是否会成为C
14,或者它是否会独立发布,但GCC已经seems to support this了。

ogsagwnx

ogsagwnx2#

我想std::is_base_of可以帮助你

#include <type_traits>

std::is_base_of<B, D>()

字符串
如果D是从B派生的,或者两者都是同一个非联合类,则提供等于true的成员常数值。否则value为false。
你可以用它来检查一个类是否是另一个类的基类:

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

vhmi4jdf

vhmi4jdf3#

这可能是一种很好的方法,具体取决于您的用例。在基类本身中声明名为base的基类的typedef。
然后派生类X将继承它作为类型名X::base
所以B::baseAC::baseA

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

字符串

ldxq2e6h

ldxq2e6h4#

在一定的限制下,这是可能的!

  • 需要以这种方式可检测的每个碱基必须从某个CRTP碱基继承。(或者,可能包含某种宏。)
  • 你会得到一个所有父母的列表,包括间接父母。

在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
}

字符串
事情是这样的:

  • 您可以使用有状态模板元编程来创建一个类型列表,您可以通过示例化特定的模板来向该列表添加类型。
  • 使用CRTP基,您可以向需要以这种方式检测的每个类添加friend函数。这些函数被SFINAE设置为不可调用,但是在重载解析时只要考虑它们就会示例化模板,该模板会将相应的基类追加到列表中。
  • 您可以使用ADL调用这个重载的函数,并且由于考虑并尝试示例化来自所有基类的重载,因此您将获得一个基类列表。
k2fxgqgv

k2fxgqgv5#

@HolyBlackCat的回答很精彩,但不透明。这是我试图解释它。
通过程序一步一步地工作:

  • 我们的第一个把戏:friend类中的函数可以在类中声明 * 和定义 *。这实质上是将一个 * 非成员 * 函数添加到类的作用域中。
  • 如果我们在一个类 template 中定义friend函数,我们可以访问用于定义该专门化的特定模板参数,并且我们可以将这些参数放在编译器和其他模板可以看到的地方。
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>{};}
    };
}

字符串
现在,通过将BaseWriter的示例放入我们想要检测的D的基类中,我们可以使编译器将adl_ViewBase的相应重载添加到类作用域中,该作用域将基类的类型编码为函数的返回类型。

  • 由于它实际上不是一个成员函数,我们可以一般地引用adl_ViewBase并强制ADL创建一个候选池,该池由我们选择的函数组成。
  • 如果我们要将这个模板级的元级代码Map到熟悉的对象级C++,这一步将本质上定义一个类,我们可以称之为“非链接列表”,我们可以向其添加新元素。不过,我们还没有办法进入它。
  • 第二招:如果我们实际上试图在上面的代码上构建太多,就会遇到问题,因为类和友元函数上的模板交互方式很微妙。为了使特定的友元函数具有对类模板的特定特化的访问权限,在模板化类中定义的每个友元函数默认情况下都是区分的,尽管它们隐式地是inline,因此如果它们具有相同的名称和调用签名,它们将被视为违反ODR。我们可以通过为每个特定的模板专用函数签名提供一个正向声明来恢复“正常”行为,这是通过在具有类似模板参数的模板中提供声明最容易实现的。
//...
    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>{};}
    };

  • 不过,我们不应该抱怨,因为在这个过程中,我们将创建一个简单的方法来实际获取我们稍后提取的信息。假设我们可以(a)找到某种方法将I的唯一值传递到各种BaseWriter中,并且(B)跟踪有效值的总数,我们可以通过查询viewBase的返回类型来系统地检索所有函数(以及所有类型标记)。
  • 换句话说,前一步的“非链接列表”在编译器的内部工作中被组织成一种原始的“Map”,编号的标记结构BaseViewer充当允许我们查看特定索引库的键。(因此命名。)像一个Map,而不像一个数组或列表,我们必须处理为我们的数据自己创建唯一的键。不过,那是以后的事了。现在...
  • 第三招:由于处理friend函数是模板专门化的一部分,因此模板示例不一定需要实际构造来填充数组中它的部分。然后,我们可以利用C++规范的某个特性,该特性与编译器必须为哪些可能的专门化实际生成代码有关。如cppreference.com,

当代码在需要函数定义存在的上下文中引用函数时,或者如果定义的存在影响程序的语义(自C++11起),并且此特定函数尚未显式示例化,则会发生隐式示例化。如果可以从上下文推导出模板参数列表,则不必提供该列表。
函数定义的存在被认为会影响程序的语义,如果函数是表达式 * 需要常量求值 即使 * 表达式的常量求值不是必需的,或者如果常量表达式求值不使用定义。
(强调我的)。特别是,非类型模板参数总是被标准认为是“明显的常量求值表达式”,因此将BaseWriter的特定特化包含到模板参数中足以迫使编译器为其生成代码(从而产生友元函数,从而填充我们的“Map”),即使模板最终未被使用。

  • 我们将其添加到SFINAE的标准应用程序中,以确保我们创建的函数模板永远不会被使用,因此永远不会进入运行时评估。现在我们可以将我们的基础引入到CRTP基础中,我们的实际基础可以继承以触发我们上面构建的结构:
#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 {};

在这里,T表示我们的一个基本类型,它本身将以CRTP方式继承自BaseD是我们要查询的派生类。就像普通的&&||一样,如果模板替换遇到替换失败,则会短路,因此如果D没有派生T,编译器会停止其替换尝试。只有当enable_if通过时,它才会尝试示例化最后一个参数,这会导致编译器将T追加到我们正在构建的数组中。nullptr用作忽略参数的通用单元类型。

  • 由于我们在类模板中再次声明和定义了一个友元成员,所以每次我们示例化一个从Base派生的类CRTP时,编译器都会自动将一个对应的registerBases函数模板添加到封闭作用域中。
  • 当然,顾名思义,对于T, D的任何值,BaseWriter实际上都没有名为nonExistent的成员,因此替换总是失败。因此,在最终运行时中不会有直接对应于registerBases的代码。
  • 最后一招,在我们把所有东西绑起来之前:ADL查找。当我们通过名称调用函数时,ADL必须首先构建该名称的所有可能定义(重载)的列表。如果遇到同名的函数模板,则必须尝试示例化该模板,然后才能知道是否应将该定义添加到候选者池中。这意味着仅仅考虑一个模板就足以激活我们上面所拥有的所有机制。我们所要做的就是在某个地方添加一个ADL虚拟目标,一旦编译器意识到我们给它的模板都没有真正通过替换,它就可以依靠它。我们将名称更改为adl_RegisterBases以记录我们的意图,并将我们得到的所有内容放在一起:
#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);
}


现在,我们要做的就是弄清楚我们最后留下的那一点:以一种系统的方式给每个碱基分配不同的数字,我们可以在最后复制。最简单的方法是以某种方式跟踪每个目标(派生类)的“map”的大小,并将map的最新大小作为其索引分配给每个新基类。
所以,我们需要一个基本情况和一般结构的东西,看起来像...

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


一个特定的NumBases规范只能有一个::value,这是一个静态属性,在编译过程中不能改变;如果我们打算使用类型模板NumBases来跟踪我们的Map的增长长度,这是一个问题。因此,我们构建NumBases,将其本身表示为特定的basetype-index对<Next, I>,并通过将::value传递到BaseWriter来将其附加到特定的BaseViewer,具体为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 *) {}
};


至此,我们的代码已经运行并编译完毕,可以开始学习C++ Insights了。
请注意,BaseWriter生成了一个特殊化BaseWriter<A,0,A>NumBases生成了一个特殊化NumBases<A, A, 0>。专门化BaseWriter<A,0,A>又生成了BaseViewer<A, 0>和一个专门接受BaseViewer<A, 0>viewBase重载。我们可以利用我们的老朋友SFINAE:我们可以对NumBases进行特殊化,作为一个特殊化,它总是首先被检查,但只有当具有正确索引值I的相应BaseViewer(忽略Next)已经被特殊化时,才会通过替换:

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


如果我们递归地定义NumBases本身,但是用I+1替换I,那么当特殊化停止应用时,递归将终止--也就是说,当编译器用完了具有适当索引的现有BaseViewer时。作为一个副作用,通过首先填充我们的“map”的相同机制,ADL的考虑也将示例化BaseViewer<D, I+1>,增加计数器并为下一次调用NumBases{}做好准备。
为了清楚起见,我们将SFINAE逻辑移到它自己的模板类型参数中,因此NumBases现在定义为

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> {};


这就是最难的部分:我们可以通过C++ Insights确认NumBases完成了它的工作,并为我们提供了从BaseViewer<D, 0>BaseViewer<D, 3>以及从BaseWriter<D, 0, A>BaseWriter<D, 3, D>的密钥(用于名为D的测试结构)。我们使用std::index_sequence和参数包扩展来枚举我们的“map”的条目,将其打包到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());
    };


......清理我们的代码一些(打包直接调用adl_RegisterBases,其非常具体的<T>((T *)nullptr)语法,到自己的函数;创建一个helper struct impl::RegisterBases,它隐藏了一些在Base中调用的实现细节-这意味着我们必须将adl_RegisterBases本身移动到impl中,以便来自RegisterBases的调用可以找到它):

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 *) {}
};


...并消除了GCC关于我们的非模板化友元函数的一些警告(可能正是警告我们在第二步中必须处理的奇怪的inline失败行为)。

然后代码运行。要使用它,请使用CRTP将样板adl_RegisterBases friend模板包含在您想要检测的所有碱基中,然后对于任何类型的T,只需获取base_list<T>的类型。
在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
}

相关问题