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

Yan*_*hou 24 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();
}
Run Code Online (Sandbox Code Playgroud)

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

我明白为什么它会编译运行,如果我错了就纠正我.

当我们拨打电话d2.call(),呼叫决心Base<D1>::call,并会投thisD1和调用D1::call_impl.因为D1确实是派生形式Base<D1>,所以演员在编译时很好.

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

关键是,这段代码虽然内涵错误,但不会给用户带来任何错误信号.我在我的项目中真正做的是我有一个CRTP基类,就像一个调度引擎.在图书馆查阅另一类CRTP基类的接口,也就是说call,并call会分派到call_dispatch它可以是基类的默认实现或派生类的实现.如果用户定义的派生类D确实派生自,那么这些都可以正常工作Base<D>.如果它是从未派生的Base<Unrelated>地方派生的,它将引发编译时错误.但它不会阻止用户编写如上所述的代码.UnrelatedBase<Unrelated>

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

所以我的问题是,有什么办法可以阻止用户编写不正确的派生类,如上所述.也就是说,如果用户编写派生的实现类,比如说D,但是他从中派生出来Base<OtherD>,则应该引发编译时错误.

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

use*_*672 33

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();
  }
};
Run Code Online (Sandbox Code Playgroud)

在此之后,将无法创建错误的继承D2的任何实例.

  • @ user396672:我能看到的唯一陷阱是它在C++ 11之前是不正确的.明确禁止使用类模板的标准将模板类型参数声明为朋友. (2认同)

Pla*_*aHH 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);
}
Run Code Online (Sandbox Code Playgroud)

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