关于c ++:默认将类设为`final`或者给它们一个虚拟析构函数?

Default to making classes either `final` or give them a virtual destructor?

如果将具有非虚拟析构函数的类用作基类(如果使用指向基类的指针或引用引用子类的实例),则这些类是错误的源。

在C++ 11中添加了EDCOX1和0的类,我想知道下面的规则是否成立是有意义的:

每个类必须满足以下两个属性之一:

  • 标记为final(如果还没有打算继承)
  • 有一个虚拟析构函数(如果它是(或打算)继承自)
  • 可能有两种选择都不合理的情况,但我想它们可以作为例外处理,应该仔细记录。


    我经常问自己的问题是,是否可以通过类的接口删除该类的实例。如果是这样的话,我将它公开化和虚拟化。如果不是这样,我会保护它。如果通过接口多态地调用析构函数,则类只需要一个虚拟析构函数。


    由于缺少虚拟析构函数,最常见的实际问题可能是通过指向基类的指针删除对象:好的。

    1
    2
    3
    4
    5
    struct Base { ~Base(); };
    struct Derived : Base { ~Derived(); };

    Base* b = new Derived();
    delete b; // Undefined Behaviour

    虚拟析构函数也会影响释放函数的选择。vtable的存在也会影响type_iddynamic_cast。好的。

    如果您的类没有以这些方式使用,那么就不需要虚拟析构函数。请注意,此用法不是类型的属性,既不属于Base类型,也不属于Derived类型。继承使这样的错误成为可能,而只使用隐式转换。(通过明确的转换,如reinterpret_cast,类似的问题在没有继承的情况下是可能的。)好的。

    通过使用智能指针,您可以在许多情况下避免这个特定的问题:unique_ptr类类型可以限制转换为具有虚拟析构函数(*)的基类的基类。类shared_ptr类型可以存储适合删除指向Bshared_ptr的删除程序,即使没有虚拟析构函数。好的。

    (*)虽然当前的std::unique_ptr规范没有包含转换构造函数模板的检查,但它在早期的草案中受到了限制,请参见LWG854。提案N3974介绍了checked_delete删除程序,它还需要一个虚拟DTOR来进行派生到基的转换。基本上,您可以防止转换,例如:好的。

    1
    2
    3
    4
    unique_checked_ptr<Base> p(new Derived); // error

    unique_checked_ptr<Derived> d(new Derived); // fine
    unique_checked_ptr<Base> b( std::move(d) ); // error

    正如N3974所建议的,这是一个简单的库扩展;您可以编写自己版本的checked_delete,并将其与std::unique_ptr结合起来。好的。

    OP中的两个建议都可能存在性能缺陷:好的。

  • 将一个类标记为final
  • 这将阻止空基优化。如果有空类,则其大小必须大于等于1字节。因此,作为数据成员,它占用空间。但是,作为一个基类,它不允许占用派生类型对象的独特内存区域。这用于将分配器存储在stdlib容器中。好的。

  • 有一个虚拟析构函数
  • 如果类还没有vtable,这将为每个类引入vtable,并为每个对象添加vptr(如果编译器不能完全消除它)。对物体的破坏会变得更加昂贵,这会产生影响,例如,因为它不再是微不足道的可破坏性。此外,这可以防止某些操作,并限制可以对该类型执行的操作:对象的生存期及其属性链接到该类型的某些属性,如可销毁的属性。好的。

    final防止通过继承来扩展类。虽然继承通常是扩展现有类型的最糟糕方法之一(与自由函数和聚合相比),但在某些情况下继承是最合适的解决方案。final限制了该类型可以做什么;应该有一个非常有说服力和根本性的原因,我应该这样做。人们通常无法想象其他人使用您的类型的方式。好的。

    T.C.指出了stdlib的一个例子:源自std::true_type,类似地,源自std::integral_constant(例如占位符)。在元编程中,我们通常不关心多态性和动态存储时间。公共继承通常只是实现元函数的最简单方法。我不知道任何情况下元函数类型的对象是动态分配的。如果这些对象是完全创建的,那么它通常用于标记调度,在这里您将使用临时的。好的。

    作为替代方案,我建议使用静态分析仪工具。无论何时从没有虚拟析构函数的类中公开派生,都可以引发某种类型的警告。请注意,在许多情况下,您仍然希望在没有虚拟析构函数的情况下从一些基类公开派生;例如,干性或简单地分离关注点。在这些情况下,静态分析器通常可以通过注释或pragma进行调整,以忽略从类w/o虚拟dtor派生的这种情况。当然,对于外部库(如C++标准库),需要有例外。好的。

    如果删除了类A的一个对象,而类B是从类A继承来的,那么分析起来会更好,但更复杂。不过,这种检查可能不可靠:删除可能发生在与定义B的tu不同的翻译单元中(从A派生)。它们甚至可以在单独的库中。好的。好啊。


    好吧,严格地说,只有当指针被删除或对象被破坏(仅通过基类指针)时,才会调用UB。

    在API用户不能删除对象的情况下,可能会有一些例外,但除此之外,这通常是一个明智的规则。