【问题标题】:Prevent user from deriving from incorrect CRTP base防止用户从不正确的 CRTP 基础派生
【发布时间】:2012-06-27 11:08:03
【问题描述】:

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

考虑下面的代码

#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_ 未初始化而预期的。第二次和第三次调用都会为data_ 输出100

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

当我们调用 d2.call() 时,调用被解析为 Base&lt;D1&gt;::call,这会将 this 转换为 D1 并调用 D1::call_impl。因为D1 确实是从Base&lt;D1&gt; 派生的,所以在编译时转换是可以的。

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

关键是,这段代码,虽然在本质上是错误的,但不会给用户任何错误的迹象。我在我的项目中真正做的是我有一个 CRTP 基类,它就像一个调度引擎。库中的另一个类访问 CRTP 基类的接口,比如callcall 将分派到call_dispatch,它可以是基类默认实现或派生类实现。如果用户定义的派生类,比如D,确实是从Base&lt;D&gt; 派生的,那么这些都可以正常工作。如果它是从Base&lt;Unrelated&gt; 派生的,其中Unrelated 不是从Base&lt;Unrelated&gt; 派生的,它将引发编译时错误。但它不会阻止用户编写上述代码。

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

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

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

【问题讨论】:

  • 简短回答:不。 dynamic_cast 可能很昂贵,但比尝试修复用户要便宜。
  • 至少在单元测试中会更快地发现失败的 dynamic_cast。你不能让编译器保护你免受所有可能的错字,比如写i + 1,意思是i - 1。这是相似的。

标签: c++ crtp


【解决方案1】:

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

【讨论】:

  • 我认为这行得通!为什么这些真正聪明的东西总是很简单!这看起来很像 Barton–Nackman 的把戏
  • @Yan Zhou:我试过了,它似乎有效:) 我也刚刚在 SO:stackoverflow.com/questions/5907731/… 上找到了一个相关的问题并回答了它,但是,也许有一些陷阱,因为问题似乎更笼统(仅与错误继承有关)。
  • @user396672:我能看到的唯一缺陷是它在 C++11 之前格式不正确。该标准明确禁止类模板将模板类型参数声明为友元。
  • 这种方法的问题在于它不允许更复杂的场景,其中 Derived 不是 Base 的直接后代。
  • 您甚至可以将 Base 私人 ctor 标记为默认值,输入更多但也可以:)
【解决方案2】:

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

这一切都发生在 Base 中,它所拥有的只是 Derived 的信息。它永远不会有机会查看调用上下文是来自 D2 还是 D1,因为这没有区别,因为 Base&lt;D1&gt; 以一种特定方式被实例化一次,无论它是由 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% 的保证,但在实践中这通常会发现人们犯的所有错误。

【讨论】:

  • 我也有同样的想法,但这并不像看起来那么容易。如果您在 Base 的主体中使用它,就像这样 static_assert( is_base_of&lt;Base&lt;Derived&gt;, Derived&gt;::value, "...") 这将不起作用,因为如果模板 get 用 D1 实例化,这将变为 static_assert( is_base_of&lt; Base&lt; D1 &gt;, D1 &gt;::value, "...") 不会触发断言(毕竟 D1 是源自Base&lt; D1 &gt;)。此捕获的唯一错误是从Base&lt; D3 &gt; 派生的错误,其中D3 本身并不从正确的Base 继承。但是,static_cast 已经捕获了这些错误。
  • @LiKao:啊,现在我明白你的意思了,让我在答案中补充一下。
  • 感谢多投提示。我只是想出了一个非常相似的想法。我在调试模式下断言dynamic_cast,然后执行static_cast。模板解决方案显然也很优雅。
  • +1 表示 poly cast。这种类型的演员(或类似的方式,如assert_cast)非常好,它们应该成为标准恕我直言的一部分。
  • 此解决方案要求Base 是多态类型。使用原始发布的代码,其中Base 不是多态的,动态转换本身将无法编译。
【解决方案3】:

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

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

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

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

【讨论】:

  • 我在这里找不到任何可以回答他的问题的东西,只能为贵公司宣传......
  • 来吧。我是说: 1. C++2003 没有为错误的模板实例化提供良好的保护。 2. C++11 可能会有所帮助。 3. 算法分析可能会有所帮助。不一定要借助我们的工具。我在哪里说其他工具不起作用?
  • 他在问 /what/ 有帮助,确切地说。告诉我那里有一些可能有帮助的东西,我猜他已经想通了
  • 我的主要建议是在 C++2003 中寻找保护模板的方法不是正确的方法。有关于这方面的文章甚至书籍。
【解决方案4】:

如果你不能用 C++11 计算,你可以试试这个技巧:

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

    静态派生 *派生( ) { 返回空值; }

  2. 将静态check 函数模板添加到接受指针的基础:

    模板 静态布尔检查( T *derived_this ) { return (derived_this == Base::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]                                                                                                                                          

【讨论】:

    【解决方案5】:

    一般来说,我不认为有办法得到这个,不应该被认为是完全丑陋的,并恢复到使用邪恶的功能。以下是可行和不可行的摘要。

    • 使用static_assert(来自C++11 或来自boost)不起作用,因为检查Base 的定义只能使用Base&lt;Derived&gt;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&lt; D1 &gt;,静态断言将无法捕捉到这一点,因为D1 实际上是从Base&lt; D1 &gt; 派生的,并且静态断言完全有效。但是,如果您从Base&lt; D3 &gt; 派生,其中D3 是不是从Base&lt; D3 &gt; 派生的任何类,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 可能是有意义的(有一篇关于 Dr. Jobbs 的文章),当dynamic_cast 失败时它会自动触发一个失败的断言。

    【讨论】:

      【解决方案6】:

      没有办法防止用户编写不正确的派生类;但是,有一些方法可以防止您的代码调用具有意外层次结构的类。如果用户在某些点将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 ) 将无法编译。编译器错误可能不像用户希望的那样明确,因此可能值得考虑静态断言。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2021-09-10
        • 1970-01-01
        • 2021-10-25
        相关资源
        最近更新 更多