默认为使类为"final"或给它们一个虚拟析构函数?

Sim*_*mon 6 c++ final virtual-destructor c++11

具有非虚拟析构函数的类如果它们被用作基类(如果使用指向基类的指针或引用来引用子类的实例),则它们是错误的来源.

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

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

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

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

dyp*_*dyp 7

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

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

Base* b = new Derived();
delete b; // Undefined Behaviour
Run Code Online (Sandbox Code Playgroud)

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

如果您的类不以这些方式使用,则不需要虚拟析构函数。请注意,这种用法是不是一个类型的属性,无论类型的BaseNOR型的Derived。继承使这样的错误成为可能,而只使用隐式转换。(对于诸如 之类的显式转换,在reinterpret_cast没有继承的情况下可能会出现类似的问题。)

通过使用智能指针,您可以在许多情况下防止出现此特定问题:unique_ptr-like 类型可以限制转换为具有虚拟析构函数(*)的基类的基类。shared_ptr样类型可以存储一个删除器适于删除shared_ptr<A>该点到B即使没有虚拟析构函数。

(*)虽然 的当前规范std::unique_ptr不包含对转换构造函数模板的此类检查,但它在较早的草案中受到限制,请参阅LWG 854。提案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
Run Code Online (Sandbox Code Playgroud)

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


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

  1. 将班级标记为 final

这可以防止空基优化。如果你有一个空类,它的大小仍然必须 >= 1 字节。作为数据成员,它因此占用空间。但是,作为基类,不允许占用派生类型对象的不同内存区域。这用于例如在 StdLib 容器中存储分配器。 C++20 通过引入[[no_unique_address]].

  1. 有一个虚拟析构函数

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


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

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


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

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

  • 我希望这样的静态分析器至少可以被教导忽略从 `std::true_type` 和 `std::false_type` 派生。 (2认同)

Wer*_*mus 5

我经常问自己的问题是,类的实例是否可以通过其接口删除。如果是这种情况,我会将其公开并虚拟化。如果不是这种情况,我会将其保护起来。如果析构函数将通过其接口多态调用,则类仅需要虚拟析构函数。