使用虚拟析构函数使非虚函数进行v表查找吗?

时间:2010-10-13 12:15:10

标签: c++ oop crtp

主题是什么问题。还想知道为什么CRTP的常见示例中没有提到virtual dtor。

编辑: 伙计们,请发表关于CRTP问题的文章,谢谢。

4 个答案:

答案 0 :(得分:5)

只有虚拟函数需要动态调度(因此 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兼容。另一个解决方案是将typedef更改为base3,在这种情况下,通过让编译器在该行上大喊来解决问题。缺点是你不能通过指针删除base,优点是编译器可以静态地确保不存在未定义的行为。

在CRTP模式的特定情况下(借用冗余的模式),大多数作者甚至不关心破坏者是否受到保护,因为意图不是通过以下方式保存派生类型的对象:对基数(模板化)类型的引用。为了安全起见,他们应该将析构函数标记为受保护,但这很少是一个问题。

答案 1 :(得分:4)

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

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

例如,在

template <class Derived> class Base
{
};

class Derived : Base<Derived>
{
};

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

答案 2 :(得分:4)

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

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中,实际上在Bar的vtable中,在这个例子中你需要两个不同的插槽用于Foo::foo()和{ {1}}。这意味着即使实现想要这样做,它也将是vtable的特例使用。在实践中它不想这样做,做它没有意义,不用担心它。

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

如果类的用户可能获取指向对象的指针,将其强制转换为基类指针类型,然后将其删除,则需要虚拟析构函数。虚拟析构函数意味着这将起作用。基类中受保护的析构函数会阻止它们尝试它(Bar::foo()将无法编译,因为没有可访问的析构函数)。因此,虚拟或受保护中的任何一个都可以解决用户意外引发未定义行为的问题。

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

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

没有用户会创建自己的delete对象,这不是Base<Derived>对象,因为这不是CRTP基类的用途。他们只是不需要能够访问析构函数 - 所以你可以将它从公共界面中删除,或者保存一行代码,你可以将它公开,并依赖用户不做傻事。

不希望它是虚拟的,因为它不需要,只是如果它不需要它们就没有给出类虚函数。有一天它可能会花费一些东西,在对象大小,代码复杂性甚至(不太可能)的速度方面,所以将虚拟化的东西永远都是过早的悲观化。使用CRTP的那种C ++程序员的首选方法是绝对清楚什么是类,它们是否设计为基类,如果是,它们是否被设计为用作多态基。 CRTP基类不是。

用户没有业务转换到CRTP基类的原因,即使它是公共的,是因为它没有真正提供“更好”的接口。 CRTP基类依赖于派生类,因此如果将Derived强制转换为Derived*,则不会转换为更通用的接口。没有其他类会将Base<Derived>*作为基类,除非它还有Base<Derived>作为基类。它只是作为多态基础没用,所以不要把它作为一个。

答案 3 :(得分:0)

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

但是,这只是我疯了还是社区出现严重错误?我觉得有点害怕看到这么多人暗示拿着Base的指针/引用是没用的/很少见的。上面的一些流行答案表明我们不会与CRTP建立IS-A关系模型,我完全不同意这些观点。

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

在性能至关重要的某个领域,存在另一种方式(模式二)以可测试/可模拟的方式模拟IS-A关系 - 通过CRTP。实际上,在某些情况下,性能提升可能令人印象深刻(文章中为600%),请参阅此link。所以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的关系?