防止用户从错误的CRTP基础派生

时间:2012-06-27 11:08:03

标签: c++ crtp

我无法想出一个适当的问题标题来描述问题。希望下面的详细说明可以解释我的问题。

考虑以下代码

#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>,而Unrelated不是从Base<Unrelated>派生的,则会引发编译时错误。但它不会阻止用户编写如上所述的代码。

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

所以我的问题是,是否有任何方法可以阻止用户编写不正确的派生类,如上所述。也就是说,如果用户编写派生的实现类,比如说D,但是他从Base<OtherD>派生出来,则会引发编译时错误。

一种解决方案是使用dynamic_cast。但是,这是扩展的,即使它工作也是一个运行时错误。

6 个答案:

答案 0 :(得分:34)

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的任何实例。

答案 1 :(得分:4)

如果你有C ++ 11可用,你可以使用static_assert(如果没有,我相信你可以通过提升模仿这些东西)。你可以断言,例如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%保证,但实际上这通常可以解决人们犯下的所有错误。

答案 2 :(得分:2)

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

我知道C ++ 11有静态断言可能会有所帮助。我不知道详情。

其他观点。除编译错误外,还有静态分析。你要求的东西有一些与此有关。分析并不一定会寻找安全漏洞。它可以确保代码中没有重新计算。它可以检查某些类没有衍生物,你可以对模板和函数的参数等进行限制。这就是所有的分析。编译器不支持这种广泛变化的约束。我不确定这是正确的方法,只是说明这种可能性。

P.S。我们公司在这方面提供服务。

答案 3 :(得分:1)

如果您无法使用C ++ 11,您可以尝试这个技巧:

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

    static Derived * derived() {return NULL; }

  2. 将一个静态check函数模板添加到带有指针的基础:

    模板&LT; typename T&gt; 静态布尔检查(T * derived_this) {     return(derived_this == Base&lt; Derived&gt; :: derived()); }

  3. Dn构造函数中,请致电check( this )

    检查(这)

  4. 现在,如果您尝试编译:

    $ 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]                                                                                                                                          
    

答案 4 :(得分: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();
       }
    };
    

如果您尝试将D2声明为class D2 : Base< D1 >,静态断言将无法捕获此值,因为D1实际上是从Base< D1 >派生的,并且静态断言完全有效。但是,如果您派生自Base< D3 >,其中D3是任何不是Base< D3 >派生的类,则static_assertstatic_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(有关Jobbs博士的文章)可能是有意义的,当dynamic_cast失败时会自动触发失败的断言。

答案 5 :(得分: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 )将无法编译。编译器错误可能不像用户想要的那样明确,因此考虑静态断言可能是值得的。