25

我想不出一个合适的问题标题来描述这个问题。希望下面的详细信息可以清楚地解释我的问题。

考虑以下代码

#include <iostream>

template <typename Derived>
class Base
{
    public :

    void call ()
    {
        static_cast<Derived *>(this)->call_impl();
    }
};

class D1 : public Base<D1>
{
    public :

    void call_impl ()
    {
        data_ = 100;
        std::cout << data_ << std::endl;
    }

    private :

    int data_;
};

class D2 : public Base<D1> // This is wrong by intension
{
    public :

    void call_impl ()
    {
        std::cout << data_ << std::endl;
    }

    private :

    int data_;
};

int main ()
{
    D2 d2;
    d2.call_impl();
    d2.call();
    d2.call_impl();
}

尽管 的定义D2是故意错误的,但它会编译并运行。第一次调用将输出一些预期未初始化d2.call_impl()的随机位。D2::data_第二次和第三次调用都将输出100.data_

我明白为什么它会编译和运行,如果我错了,请纠正我。

当我们进行调用d2.call()时,调用被解析为Base<D1>::call,并且将this转换为D1和调用D1::call_impl。因为D1确实是派生形式Base<D1>,所以在编译时强制转换很好。

在运行时,在强制转换之后this,虽然它确实是一个D2对象,但它被视为好像它是D1,并且调用D1::call_impl将修改应该是的内存位D1::data_,并输出。在这种情况下,这些位恰好在哪里D2::data_。我认为第二个d2.call_impl()也应该是未定义的行为,具体取决于 C++ 实现。

关键是,这段代码虽然本质上是错误的,但不会给用户任何错误的迹象。我在我的项目中真正做的是我有一个 CRTP 基类,它就像一个调度引擎。库中的另一个类访问 CRTP 基类的接口,比如callcall并将调度到call_dispatch该接口,可以是基类默认实现或派生类实现。如果用户定义的派生类,比如说D,确实是从Base<D>. 如果它源自Base<Unrelated>whereUnrelated不是源自Base<Unrelated>. 但它不会阻止用户编写上述代码。

用户通过从基 CRTP 类派生并提供一些实现细节来使用该库。当然还有其他设计方案可以避免上述错误使用的问题(例如抽象基类)。但是让我们暂时把它们放在一边,相信我因为某种原因我需要这个设计。

所以我的问题是,有什么方法可以防止用户编写错误的派生类,如上所示。也就是说,如果用户编写了一个派生的实现类,比如D,但他是从 派生的Base<OtherD>,那么将引发编译时错误。

一种解决方案是使用dynamic_cast. 但是,这是广泛的,即使它有效,它也是一个运行时错误。

4

6 回答 6

35

1)将Base的所有构造函数设为私有(如果没有构造函数,就加一个)

2) 将 Derived 模板参数声明为 Base 的朋友

template <class Derived>
class Base
{
private:

  Base(){}; // prevent undesirable inheritance making ctor private
  friend  Derived; // allow inheritance for Derived

public :

  void call ()
  {
      static_cast<Derived *>(this)->call_impl();
  }
};

在此之后,将不可能创建错误继承 D2 的任何实例。

于 2012-06-28T08:49:32.507 回答
5

如果你有可用的 C++11,你可以使用static_assert(如果没有,我相信你可以用 boost 模拟这些东西)。您可以断言例如is_convertible<Derived*,Base*>is_base_of<Base,Derived>

这一切都发生在 Base 中,它所拥有的只是 Derived 的信息。它永远不会有机会查看调用上下文是来自 D2 还是 D1,因为这没有区别,因为Base<D1>实例化了一次,以一种特定的方式,无论它是由 D1 实例化还是由它派生的 D2 实例化(或由用户显式实例化它)。

由于您不想(可以理解,因为它有时具有显着的运行时成本和内存开销)使用 dynamic_cast,请尝试使用通常称为“poly cast”的东西(boost 也有自己的变体):

template<class R, class T>
R poly_cast( T& t )
{
#ifndef NDEBUG
        (void)dynamic_cast<R>(t);
#endif
        return static_cast<R>(t);
}

通过这种方式在您的调试/测试构建中检测到错误。虽然不是 100% 的保证,但在实践中,这通常会发现人们犯的所有错误。

于 2012-06-27T11:35:15.527 回答
2

一般要点:模板不受保护,不会被错误的参数实例化。这是众所周知的问题。不建议花时间尝试解决此问题。模板被滥用的数量或方式是无穷无尽的。在您的特定情况下,您可能会发明一些东西。稍后您将修改您的代码,新的滥用方式将出现。

我知道 C++11 有可能有帮助的静态断言。我不知道全部细节。

其他点。除了编译错误还有静态分析。您所要求的与此有关。分析不一定会寻找安全漏洞。它可以确保代码中没有recursion。它可以检查某个类是否没有派生类,可以对模板和函数的参数进行限制等。这都是分析。编译器无法支持如此广泛变化的约束。我不确定这是正确的方法,只是讲述这种可能性。

ps 我们公司提供这方面的服务。

于 2012-06-27T11:25:42.150 回答
1

如果你不能指望 C++11,你可以试试这个技巧:

  1. 添加一个静态函数,Base该函数返回一个指向其特殊类型的指针:

    static Derived *derived( ) { return NULL; }

  2. 将静态check函数模板添加到带有指针的 base 中:

    模板< typename T > static bool check( T *derived_this ) { return ( derived_this == Base< Derived >::derived( ) ); }

  3. 在您的Dn构造函数中,调用check( this )

    检查(这个)

现在,如果您尝试编译:

$ g++ -Wall check_inherit.cpp -o check_inherit
check_inherit.cpp: In instantiation of ‘static bool Base<Derived>::check(T*) [with T = D2; Derived = D1]’:
check_inherit.cpp:46:16:   required from here
check_inherit.cpp:19:62: error: comparison between distinct pointer types ‘D2*’ and ‘D1*’ lacks a cast                                                                                                                             
check_inherit.cpp: In static member function ‘static bool Base<Derived>::check(T*) [with T = D2; Derived = D1]’:                                                                                                                   
check_inherit.cpp:20:5: warning: control reaches end of non-void function [-Wreturn-type]                                                                                                                                          
于 2012-06-27T12:02:42.980 回答
1

没有办法防止用户编写不正确的派生类;但是,有一些方法可以防止您的代码调用具有意外层次结构的类。如果存在用户传递Derived给库函数的点,请考虑让这些库函数执行static_cast预期的派生类型。例如:

template < typename Derived >
void safe_call( Derived& t )
{
  static_cast< Base< Derived >& >( t ).call();
}

或者,如果有多个层次结构,请考虑以下事项:

template < typename Derived,
           typename BaseArg >
void safe_call_helper( Derived& d,
                       Base< BaseArg >& b )
{
   // Verify that Derived does inherit from BaseArg.
   static_cast< BaseArg& >( d ).call();
}

template < typename T >
void safe_call( T& t )
{
  safe_call_helper( t, t );  
}

在这两种情况下,safe_call( d1 )将编译而safe_call( d2 )将无法编译。编译器错误可能不像用户希望的那样明确,因此可能值得考虑静态断言。

于 2012-06-27T12:14:35.827 回答
1

一般来说,我不认为有办法得到这个,这不应该被认为是彻头彻尾的丑陋并恢复到使用邪恶的特征。以下是什么可行,什么不可行的总结。

  • 使用static_assert(来自 C++11 或来自 boost)不起作用,因为检查定义Base只能使用类型Base<Derived>Derived. 所以以下看起来不错,但失败了:

    template <typename Derived>
    class Base
    {
       public :
    
       void call ()
       {
          static_assert( sizeof( Derived ) != 0 && std::is_base_of< Base< Derived >, Derived >::value, "Missuse of CRTP" );
          static_cast<Derived *>(this)->call_impl();
       }
    };
    

如果您尝试将其声明D2class D2 : Base< D1 >静态断言,则不会捕捉到这一点,因为实际上是从静态断言D1派生而来的Base< D1 > ,并且静态断言是完全有效的。但是,如果您从Base< D3 >where派生D3任何不是从Base< D3 >两者派生的类static_assert以及static_cast将触发编译错误,那么这绝对没用。

由于D2您需要在代码中检查的类型Base永远不会传递给模板,因此使用的唯一方法是static_assert将其移动到其声明之后,D2这将需要实现检查的同一个人D2,这又是无用的。

解决这个问题的一种方法是添加一个宏,但这只会产生纯粹的丑陋:

#define MAKE_DISPATCHABLE_BEGIN( DeRiVeD ) \
   class DeRiVeD : Base< DeRiVed > {
#define MAKE_DISPATCHABLE_END( DeRiVeD )
    }; \
    static_assert( is_base_of< Base< Derived >, Derived >::value, "Error" );

这只会变得丑陋,而且static_assert又是多余的,因为模板确保类型始终匹配。所以这里没有收获。

  • 最佳选择:忘记所有这些并使用dynamic_cast明确用于此场景的内容。如果您更频繁地需要它,那么实现您自己的可能是有意义的asserted_cast(有一篇关于 Dr. Jobbs 的文章),它会在失败时自动触发失败的断言dynamic_cast
于 2012-06-27T12:12:32.253 回答