【问题标题】:Protect CRTP pattern from stack overflowing in "pure virtual" calls保护 CRTP 模式免受“纯虚拟”调用中的堆栈溢出
【发布时间】:2017-12-23 02:39:34
【问题描述】:

考虑以下标准 CRTP 示例:

#include <iostream>

template<class Derived>
struct Base {
    void f() { static_cast<Derived *>(this)->f(); }
    void g() { static_cast<Derived *>(this)->g(); }
};

struct Foo : public Base<Foo> {
    void f() { std::cout << 42 << std::endl; }
};

int main() {
    Foo foo;
    foo.f(); // just OK
    foo.g(); // this will stack overflow and segfault
}

如果这是常规的虚拟继承,我可以将虚拟 fg 方法标记为纯类似

struct Base {
    virtual void f() = 0;
    virtual void g() = 0;
};

并得到一个关于 Foo 是抽象的编译时错误。但是 CRTP 没有提供这样的保护。我可以以某种方式实现它吗?运行时检查也是可以接受的。我考虑过将this-&gt;f 指针与static_cast&lt;Derived *&gt;(this)-&gt;f 进行比较,但没能成功。

【问题讨论】:

  • 我不知道这是否是标准定义的行为,但您可以在 static_assert 里面 Base::g 那个 &amp;Derived::g != &amp;Base&lt;Derived&gt;::g
  • @JohannesSchaub-litb 这很聪明!你应该写一个答案。
  • @Yakk 完成,谢谢。不过,Clang 的错误消息不是最理想的。

标签: c++ crtp pure-virtual virtual-functions


【解决方案1】:

你可以在编译时断言两个指向成员函数的指针是不同的,例如:

template<class Derived>
struct Base {
    void g() {
        static_assert(&Derived::g != &Base<Derived>::g,
                      "Derived classes must implement g()."); 
        static_cast<Derived *>(this)->g(); 
    }
};

【讨论】:

  • 您不确定这是否是上述评论中的标准定义行为。愿意在此答案中对此进行扩展吗?
  • @Yakk 我不确定指向基类/派生类之间成员函数的指针的规则(转换/比较)。我检查了标准(特别是 [conv.mem#2])和一些关于 SO 的问题,我非常有信心这是有效的。但是,如果您有理由相信这不是,请随时解释 - 如果这是不正确的,我会迅速删除此答案。
【解决方案2】:

这是另一种可能性:

#include <iostream>

template<class Derived>
struct Base {
    auto f() { return static_cast<Derived *>(this)->f(); }
    auto g() { return static_cast<Derived *>(this)->g(); }
};

struct Foo : public Base<Foo> {
    void f() { std::cout << 42 << std::endl; }
};

int main() {
    Foo foo;
    foo.f(); // just OK
    foo.g(); // this will not compile
}

对于 GCC,它给出了一个非常清晰的错误消息(“错误:在扣除 'auto' 之前使用 'auto Base::g() [with Derived = Foo]'”),而对于 Clang,它给出了一个可读性稍差的无限递归模板实例化 Base&lt;Foo&gt;::gg 实例化自身,但最终以错误结束。

【讨论】:

  • 这实际上很少见,根据我的经验,clang 给出的错误信息比 gcc 更糟糕。
  • clang seems to produce 的最新版本接近 gcc (clang >= 3.9.1)。
  • 错误信息可能不够完美,但您无法与解决方案的简单性争论!
  • 不错,但是否有可能将其改编为 C++11?
  • @uranix 可以应用类似的方法,但是错误消息和代码都变得更加丑陋:coliru.stacked-crooked.com/a/f7b767fcc6ce94d6
【解决方案3】:

你可以使用这个解决方案,你可以拥有纯“非虚抽象”函数,并且它尽可能映射到 CRTP 这个recommendation of H. Sutter:

template<class Derived>
struct Base
  {
  void f(){static_cast<Derived*>(this)->do_f();}
  void g(){static_cast<Derived*>(this)->do_g();}

  private:
  //Derived must implement do_f
  void do_f()=delete;
  //do_g as a default implementation
  void do_g(){}
  };

struct derived
  :Base<derived>
  {
  friend struct Base<derived>;

  private:
  void do_f(){}
  };

【讨论】:

    【解决方案4】:

    你可以考虑做这样的事情。您可以将 Derived 设为成员,并在每次实例化 Base 时直接将其作为模板参数提供,或者像我在此示例中所做的那样使用 类型别名

    template<class Derived>
    struct Base {
        void f() { d.f(); }
        void g() { d.g(); }
    private:
        Derived d;
    };
    
    struct FooImpl {
        void f() { std::cout << 42 << std::endl; }
    };
    
    using Foo = Base<FooImpl>;
    
    int main() {
        Foo foo;
        foo.f(); // OK
        foo.g(); // compile time error
    }
    

    当然Derived 不再是派生的,所以你可以为它取一个更好的名字。

    【讨论】:

    • 名称并没有改变语义那么糟糕。再也不能用Derived 代替Base&lt;Derived&gt;。所以泛型函数 a-la template&lt;typename T&gt; void foo(Base&lt;T&gt;&amp;) { } 将不再起作用。
    • @StoryTeller 它并不假装在语义上相同。它被标记为可能的(安全)替代方案,在许多情况下都很好。 (我一直都在用)
    猜你喜欢
    • 2016-06-30
    • 2023-04-07
    • 1970-01-01
    • 1970-01-01
    • 2011-06-01
    • 1970-01-01
    • 2014-07-03
    • 1970-01-01
    相关资源
    最近更新 更多