默认设置 类 为 `final` 还是给它们一个虚拟析构函数?

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

类 和 non-virtual 析构函数如果用作基础 class(如果使用指向基础 class 的指针或引用,则它们是错误的来源引用 child class).

的实例

随着 C++11 添加 final class,我想知道设置以下规则是否有意义:

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

  1. 被标记为final(如果它(还)不打算被继承)
  2. 有一个虚拟析构函数(如果它是(或打算)被继承)

可能有些情况下这两个选项都没有意义,但我想它们可以被视为应该仔细记录的例外情况。

嗯,严格来说,只有当指针被删除或对象被破坏(仅通过基class指针)时,UB才是已调用。

对于 API 用户无法删除对象的情况可能存在一些例外情况,但除此之外,这通常是一个明智的规则。

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

struct Base { ~Base(); };
struct Derived : Base { ~Derived(); };

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

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

如果您的 class 没有以这些方式使用,则 不需要 虚拟析构函数。请注意,此用法不是类型的属性,既不是Base类型也不是Derived类型。继承使这样的错误成为可能,而只使用隐式转换。 (通过 reinterpret_cast 等显式转换,没有继承也可能出现类似问题。)

通过使用智能指针,您可以在许多情况下避免这个特殊问题:类似 unique_ptr 的类型可以将转换限制为基数 class,对于基数 classes 具有虚拟析构函数 (*)shared_ptr-like 类型可以存储 deleter,适合删除指向 Bshared_ptr<A>,即使没有虚拟析构函数。

(*) 尽管 std::unique_ptr 的当前规范不包含对转换构造函数模板的这种检查,但它在早期的草案中有所限制,请参阅LWG 854. Proposal N3974 引入了 checked_delete 删除器,它还需要一个虚拟 dtor 来进行派生到基础的转换。基本上,您的想法是防止转换,例如:

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 中的两个建议可能 有性能缺陷:

  1. 将 class 标记为 final

这会阻止空碱基优化。如果你有一个空的 class,它的大小仍然必须 >= 1 个字节。作为数据成员,它因此占用space。但是,作为基数class,允许不占用派生类型对象的不同内存区域。这用于例如将分配器存储在 StdLib 容器中。 C++20 has mitigated this with the introduction of [[no_unique_address]].

  1. 有一个虚拟析构函数

如果 class 还没有 vtable,这会为每个 class 引入一个 vtable 加上每个对象的 vptr(如果编译器不能完全消除它)。对象的破坏可能变得更加昂贵,这可能会产生影响,例如因为它不再是微不足道的可破坏的。此外,这会阻止某些操作并限制可以对该类型执行的操作:对象的生命周期及其属性与该类型的某些属性相关联,例如微不足道的可破坏性。


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

T.C. 指出了 StdLib 中的一个示例:从 std::true_type 派生,类似地,从 std::integral_constant 派生(例如占位符)。在元编程中,我们通常不关心多态性和动态存储持续时间。 Public 继承通常只是实现元函数的最简单方法。我不知道元函数类型的对象是动态分配的。如果完全创建了这些对象,则通常用于标签分派,您可以在其中使用临时对象。


作为替代方案,我建议使用静态分析器工具。每当您在没有虚拟析构函数的情况下从 class 派生 publicly 时,您可能会发出某种警告。请注意,在各种情况下,您仍然希望在没有虚拟析构函数的情况下从某个基 class 公开派生;例如DRY 或简单的关注点分离。在这些情况下,静态分析器通常可以通过注释或编译指示进行调整,以忽略 这种从 class w/o 虚拟 dtor 派生的事件。当然,C++标准库等外部库也需要例外。

更好,但更复杂的是分析何时删除class A w/o虚拟dtor的对象,其中class B继承自class A(UB的实际来源)。但是,此检查可能不可靠:删除可能发生在与定义 B(从 A 派生)的 TU 不同的翻译单元中。它们甚至可以在单独的库中。

我经常问自己的问题是,是否可以通过其界面删除 class 的实例。如果是这种情况,我将其设为 public 和虚拟。如果不是这种情况,我会保护它。如果析构函数将通过其接口以多态方式调用,则 class 只需要一个虚拟析构函数。