【问题标题】:How to prevent a template class from being derived more than once?如何防止模板类被多次派生?
【发布时间】:2017-05-29 03:18:18
【问题描述】:

我有以下模板类:

template<class I>
class T : public I
{
    // ...
};

对于给定的模板参数I,这个模板类需要派生一次(并且只派生一次)。

class A : public T<U>  {};    // ok
class B : public T<V>  {};    // ok
class C : public T<U>  {};    // compile error

模板类T可以适应实现这样的行为(而类ABUV不能);但是,T 不得对派生类 ABC 有任何了解。

有没有办法防止这样的模板类被多次派生?理想情况下,在这种情况下会发出编译错误,或者至少是链接器错误。

【问题讨论】:

  • 如果你的程序中没有所有类的某种注册表,我看不出你怎么能做到这一点......
  • 无论解决方案是什么,都会涉及链接器错误。您不能在编译器错误的情况下执行此操作,因为每个翻译单元都会忽略其他翻译单元可能会实例化。
  • @Dan : T&lt;I&gt; 实现了一种需要一个子类的机制(这里不能开发);我需要一些方法来防止T&lt;I&gt; 被派生两次,这会破坏这种机制。
  • @FrançoisAndrieux :链接器错误并不理想,但无论如何都可以做到;有解决办法吗?
  • 是否可以接受以下形式的解决方案:class A : public X {}; (如果有一个不依赖于派生类的 T 很重要,那么 X 可能继承 T)?即A、B、C每一个要继承的都需要指定自己为模板。

标签: c++ templates inheritance


【解决方案1】:

如果基类T 知道其派生类的类型,这是可能的。该知识可以通过 CRTP 或通过重载标记传递给它的构造函数。这是后一种情况:

template<class I>
class T : public I
{
protected:
    template< class Derived >
    T( Derived * ) {
        static_assert ( std::is_base_of< T, Derived >::value,
            "Be honest and pass the derived this to T::T." );

然后,T::T( Derived * ) 需要做一些会导致问题的事情,如果它有两个专业化(具有不同的Derived)。朋友功能非常适合。实例化一个依赖于&lt;T, Derived&gt; 的辅助非成员类,使用一个依赖于T 但不依赖于Derivedfriend 函数。

        T_Derived_reservation< T, Derived >{};
    }
};

这是辅助类。 (它的定义应该在T之前。)首先,它需要一个基类来允许T_Derived_reservation&lt; T, Derived &gt;上的ADL找到一个没有提到Derived的签名。

template< typename T >
class T_reservation {
protected:
    // Make the friend visible to the derived class by ADL.
    friend void reserve_compile_time( T_reservation );

    // Double-check at runtime to catch conflicts between TUs.
    void reserve_runtime( std::type_info const & derived ) {
    #ifndef NDEBUG
        static std::type_info const & proper_derived = derived;
        assert ( derived == proper_derived &&
            "Illegal inheritance from T." );
    #endif
    }
};

template< typename T, typename Derived >
struct T_Derived_reservation
    : T_reservation< T > {
    T_Derived_reservation() {
        reserve_compile_time( * this );
        this->reserve_runtime( typeid( Derived ) );
    }

    /* Conflicting derived classes within the translation unit
       will cause a multiple-definition error on reserve_compile_time. */
    friend void reserve_compile_time( T_reservation< T > ) {}
};

当两个.cpp 文件声明不同的不兼容派生类时出现链接错误会很好,但我无法阻止链接器合并内联函数。因此,assert 将改为触发。 (您可能可以设法在标头中声明所有派生类,而不必担心assert 触发。)

Demo.


您已编辑说T 无法知道其派生类型。好吧,在编译时您无能为力,因为这些信息根本不可用。如果T 是多态的,那么您可以观察到动态类型是派生类AB,但不是在构造函数或析构函数中。如果派生类可靠地调用了其他一些函数,则可以挂钩:

template< typename I >
class T {
protected:
    virtual ~ T() = default;

    something_essential() {
    #ifndef NDEBUG
        static auto const & derived_type = typeid( * this );
        assert ( derived_type == typeid( * this ) &&
            "Illegal inheritance from T." );
    #endif
        // Do actual essential work.
    }
};

【讨论】:

  • 我不是这方面的专家,但是您的朋友函数是否存在由于没有相同的函数体而违反内联函数的 ODR 规则的问题?虽然如果确实是这样的话,也不是不可能解决的。
  • @GuyGreer 是的,这是故意的。这个想法是让编译器检测到 ODR 违规,但不幸的是我还没有找到在链接时这样做的方法。 (如果在单个 .cpp 文件中违反了 ODR,那么它将在模板实例化期间被检测到,这是我的第一个解决方案相对于第二个解决方案的优势。)
  • 根据N4567 11.3/6-7,类内部定义的友元函数自动成为内联函数。这意味着拥有多个定义是完全可以的,但是这些定义不同是 UB,编译器不需要发出诊断。
  • @GuyGreer 是的,这个答案没有揭示任何编译器错误。 assert ( reserve( * this ) == typeid (Derived) ) 行依赖 UB 来检测 reserve 的不同定义。如果编译器更热心地内联reserve,它很有可能会失败。或许将reserve 设为一个空函数会更好(这样定义永远不会不同,但可能会捕获每个 TU 的多个定义),然后像第二个解决方案一样进行运行时检查。
  • @GuyGreer 已更新修复。感谢您的反馈!
【解决方案2】:

我不是宏的忠实粉丝,但如果使用宏对您来说不是问题 - 您可以使用如下简单紧凑的解决方案:

#include <iostream>

template <class>
struct prohibit_double_inheritance { };

#define INHERIT(DERIVING, BASE) \
    template<> struct prohibit_double_inheritance<BASE> { };\
    struct DERIVING: BASE


template<class I>
struct T: I
{
    // ...
    static void do_something() {
        std::cout << "hurray hurray!" << std::endl;
    }
};

struct U { };
struct V { };

INHERIT(A, T<U>) {
};

//INHERIT(B, T<U>) { // cause redetinition of the struct 
//};                 // prohibit_double_inheritance<T<U>> 

int main() {
    A::do_something();
}

[live demo]

【讨论】:

  • 不太喜欢宏,但它是一个可行的解决方案。谢谢。
  • @shrike 我对这种方法的感觉完全一样
【解决方案3】:

既然您提到可以将ABC 传递给T,那么这个运行时解决方案怎么样?

#include <cassert>
#include <typeinfo>

//This class will check to see each T<X> is only instantiated with a unique Y
template <class X>
struct T_helper
{
    template <class Y>
    static void check()
    {
        if(derived_type)
            assert(*derived_type == typeid(Y));
        else
            derived_type = &typeid(Y);
    }
    static const std::type_info * derived_type;
};

template <class X>
const std::type_info * T_helper<X>::derived_type = nullptr;

template <class X, class Y>
struct T
{
    T()
    {
        T_helper<X>::template check<Y>();
    }
};

struct A : T<int, A> {};
struct B : T<int, B> {};

int main()
{
    A a1, a2, a3; // These are all okay
    B b1;         // This one will trigger the assert
}

【讨论】:

  • 在多线程上下文中注意derived_type 上的竞争条件。
  • @FrançoisAndrieux 当然,此代码用于说明目的
  • 简单而高效的解决方案在运行时完美运行。 (不明白为什么它没有更多的赞成票。)谢谢。
【解决方案4】:

我能想到的唯一答案是拥有一个实例化及其衍生物的注册表。然后,您可以设计一个搜索注册表的元函数。您将基类和派生类传递给它,如果您不是使用注册类型派生,它会返回 void 类型或其他东西,因此会导致编译器错误。根据您的要求,即基地不知道派生的知识,这是我能想到的唯一可能的答案。

您的声明将如下所示:

struct A : search_registry<T<U>, A>::type { ... };

我相信你会遇到很多即使在这里也很难解决的问题——祝你好运!

请记住,模板元程序纯粹是功能性的。您不能以任何好的方式“添加”到注册表。您将需要定义一次以保存所有内容。从好的方面来说,如果有人忘记添加他们会知道的注册表,或者如果他们使用以前定义的注册表......

【讨论】:

    猜你喜欢
    • 2014-01-09
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-05-26
    • 2016-02-17
    • 2015-04-10
    • 1970-01-01
    • 2017-07-05
    相关资源
    最近更新 更多