6

正是主题所要求的。还想知道为什么 CRTP 的常见示例中没有提到virtualdtor。

编辑:伙计们,请也发布有关 CRTP 问题的信息,谢谢。

4

4 回答 4

6

只有虚函数需要动态调度(因此需要vtable查找),甚至在所有情况下都不需要。如果编译器能够在编译时确定方法调用的最终覆盖器是什么,它可以省略在运行时执行分派。如果需要,用户代码也可以禁用动态调度:

struct base {
   virtual void foo() const { std::cout << "base" << std::endl; }
   void bar() const { std::cout << "bar" << std::endl; }
};
struct derived : base {
   virtual void foo() const { std::cout << "derived" << std::endl; }
};
void test( base const & b ) {
   b.foo();      // requires runtime dispatch, the type of the referred 
                 // object is unknown at compile time.
   b.base::foo();// runtime dispatch manually disabled: output will be "base"
   b.bar();      // non-virtual, no runtime dispatch
}
int main() {
   derived d;
   d.foo();      // the type of the object is known, the compiler can substitute
                 // the call with d.derived::foo()
   test( d );
}

关于是否应该在所有继承情况下提供虚拟析构函数,答案是否定的,不一定。delete仅当派生类型的代码对象通过指向基类型的指针保存时,才需要虚拟析构函数。共同的规则是你应该

  • 提供公共虚拟析构函数或受保护的非虚拟析构函数

规则的第二部分确保用户代码不能通过指向基的指针删除您的对象,这意味着析构函数不必是虚拟的。好处是,如果你的类不包含任何虚方法,这不会改变你的类的任何属性——当添加第一个虚方法时,类的内存布局会改变——并且你将保存 vtable 指针在每种情况下。从两个原因来看,第一个是重要的。

struct base1 {};
struct base2 {
   virtual ~base2() {} 
};
struct base3 {
protected:
   ~base3() {}
};
typedef base1 base;
struct derived : base { int x; };
struct other { int y; };
int main() {
   std::auto_ptr<derived> d( new derived() ); // ok: deleting at the right level
   std::auto_ptr<base> b( new derived() );    // error: deleting through a base 
                                              // pointer with non-virtual destructor
}

main 最后一行的问题可以通过两种不同的方式解决。如果typedef更改为,base1则析构函数将正确地分派给derived对象,并且代码不会导致未定义的行为。代价是derived现在需要一个虚拟表并且每个实例都需要一个指针。更重要的derived是,布局不再兼容other. 另一种解决方案是将 更改typedefbase3,在这种情况下,通过让编译器对该行大喊大叫来解决问题。缺点是不能通过指向base的指针来删除,优点是编译器可以静态保证不会有未定义的行为。

在 CRTP 模式的特殊情况下(请原谅冗余模式),大多数作者甚至不关心使析构函数受保护,因为其目的不是通过对基(模板化)类型的引用来保存派生类型的对象。为了安全起见,他们应该将析构函数标记为受保护,但这很少成为问题。

于 2010-10-13T13:17:47.723 回答
5

确实非常不可能。标准中没有任何内容可以阻止编译器执行整个类的愚蠢低效的事情,但是非虚拟调用仍然是非虚拟调用,无论该类是否也具有虚拟函数。它必须调用对应于静态类型的函数版本,而不是动态类型:

struct Foo {
    void foo() { std::cout << "Foo\n"; }
    virtual void virtfoo() { std::cout << "Foo\n"; }
};
struct Bar : public Foo {
    void foo() { std::cout << "Bar\n"; }
    void virtfoo() { std::cout << "Bar\n"; }
};

int main() {
    Bar b;
    Foo *pf = &b;  // static type of *pf is Foo, dynamic type is Bar
    pf->foo();     // MUST print "Foo"
    pf->virtfoo(); // MUST print "Bar"
}

因此,实现绝对不需要将非虚拟函数放入 vtable 中,实际上在 vtable 中,Bar在此示例中您需要两个不同的插槽 forFoo::foo()Bar::foo()。这意味着即使实现要这样做,它也将是 vtable 的特殊用途。在实践中它不想这样做,这样做没有意义,不用担心。

CRTP 基类确实应该具有非虚拟和受保护的析构函数。

如果类的用户可能需要一个指向对象的指针,将其转换为基类指针类型,然后删除它,则需要一个虚拟析构函数。虚拟析构函数意味着这将起作用。基类中的受保护析构函数阻止他们尝试它(delete由于没有可访问的析构函数,因此无法编译)。因此,virtual 或 protected 之一解决了用户意外引发未定义行为的问题。

请参阅此处的准则 #4,并注意本文中的“最近”是指近 10 年前:

http://www.gotw.ca/publications/mill18.htm

没有用户会创建Base<Derived>自己的对象,这不是Derived对象,因为这不是 CRTP 基类的用途。他们只是不需要能够访问析构函数-因此您可以将其保留在公共接口之外,或者为了保存一行代码,您可以将其保留为公共并依赖用户不会做一些愚蠢的事情。

考虑到它不需要是虚拟的,它是不可取的原因是,如果它不需要它们,那么给一个类虚拟函数是没有意义的。有一天,它可能会在对象大小、代码复杂性甚至(不太可能)速度方面付出一些代价,因此将事物始终虚拟化是一种过早的悲观情绪。使用 CRTP 的 C++ 程序员的首选方法是绝对清楚类的用途,它们是否被设计为基类,如果是,它们是否被设计为用作多态基类。CRTP 基类不是。

用户没有业务转换到 CRTP 基类(即使它是公共的)的原因是它并没有真正提供“更好”的接口。CRTP 基类依赖于派生类,因此如果转换Derived*Base<Derived>*. 任何其他类都不会Base<Derived>作为基类,除非它也Derived作为基类。它只是作为多态基础没有用,所以不要让它成为一个。

于 2010-10-13T13:09:22.930 回答
4

第一个问题的答案:不。只有对虚拟函数的调用才会在运行时通过虚拟表引起间接。

您的第二个问题的答案:Curiously recurring 模板模式通常使用私有继承来实现。您没有对“IS-A”关系建模,因此您不会将指针传递给基类。

例如,在

template <class Derived> class Base
{
};

class Derived : Base<Derived>
{
};

您没有需要 aBase<Derived>*然后继续对其调用 delete 的代码。因此,您永远不要尝试通过指向基类的指针来删除派生类的对象。因此,析构函数不需要是虚拟的。

于 2010-10-13T12:23:10.087 回答
1

首先,我认为 OP 问题的答案已经得到了很好的回答 - 这是一个可靠的 NO。

但是,是我发疯了还是社区出了严重问题?看到这么多人暗示持有 Base 的指针/引用是没用/很少见的,我感到有点害怕。上面的一些流行答案表明我们没有用 CRTP 模拟 IS-A 关系,我完全不同意这些观点。

众所周知,C++ 中没有接口之类的东西。因此,为了编写可测试/可模拟的代码,很多人使用 ABC 作为“接口”。例如,你有一个函数void MyFunc(Base* ptr),你可以这样使用它:MyFunc(ptr_derived). 这是建模 IS-A 关系的传统方法,当您在 MyFunc 中调用任何虚函数时需要 vtable 查找。所以这是对 IS-A 关系建模的模式一。

在某些性能至关重要的领域中,存在另一种方式(模式二)以可测试/可模拟的方式对 IS-A 关系进行建模 - 通过 CRTP。确实,在某些情况下,性能提升可能令人印象深刻(文章中为 600%),请参阅此链接。所以 MyFunc 看起来像这样template<typename Derived> void MyFunc(Base<Derived> *ptr)。使用 MyFunc 时,MyFunc(ptr_derived);编译器将为 MyFunc() 生成与参数类型 ptr_derived - 最匹配的代码副本MyFunc(Base<Derived> *ptr)。在 MyFunc 内部,我们很可能假设调用了接口定义的某个函数,并且在编译时静态转换了指针(查看链接中的 impl() 函数),没有 vtable 查找的开销。

现在,有人可以告诉我,我是在胡说八道,还是上面的答案根本没有考虑第二种模式来模拟 IS-A 与 CRTP 的关系?

于 2015-12-21T10:58:48.477 回答