简体   繁体   English

默认为类创建 `final` 或给它们一个虚拟析构函数?

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

Classes with non-virtual destructors are a source for bugs if they are used as a base class (if a pointer or reference to the base class is used to refer to an instance of a child class).如果将具有非虚拟析构函数的类用作基类(如果使用对基类的指针或引用来引用子类的实例),则它们是错误的来源。

With the C++11 addition of a final class, I am wondering if it makes sense to set down the following rule:随着 C++11 添加final类,我想知道制定以下规则是否有意义:

Every class must fulfil one of these two properties:每个类都必须满足以下两个属性之一:

  1. be marked final (if it is not (yet) intended to be inherited from)被标记为final (如果它不是(还)打算继承的)
  2. have a virtual destructor (if it is (or is intended to) be inherited from)有一个虚拟析构函数(如果它是(或打算)继承自)

Probably there are cases were neither of these two options makes sense, but I guess they could be treated as exceptions that should be carefully documented.可能有些情况下这两个选项都没有意义,但我想它们可以被视为应该仔细记录的例外。

The probably most common actual issue attributed to the lack of a virtual destructor is deletion of an object through a pointer to a base class:由于缺少虚拟析构函数,可能最常见的实际问题是通过指向基类的指针删除对象:

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

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

A virtual destructor also affects the selection of a deallocation function.虚拟析构函数也会影响释放函数的选择。 The existence of a vtable also influences type_id and dynamic_cast . vtable 的存在也会影响type_iddynamic_cast

If your class isn't use in those ways, there's no need for a virtual destructor.如果您的类不以这些方式使用,则不需要虚拟析构函数。 Note that this usage is not a property of a type , neither of type Base nor of type Derived .请注意,此用法不是类型的属性,既不是Base类型也不是Derived类型。 Inheritance makes such an error possible, while only using an implicit conversion.继承使这种错误成为可能,而只使用隐式转换。 (With explicit conversions such as reinterpret_cast , similar problems are possible without inheritance.) (使用诸如reinterpret_cast类的显式转换,在没有继承的情况下可能会出现类似的问题。)

By using smart pointers, you can prevent this particular problem in many cases: unique_ptr -like types can restrict conversions to a base class for base classes with a virtual destructor (*) .通过使用智能指针,您可以在许多情况下防止出现此特定问题: unique_ptr类似类型可以限制转换为具有虚拟析构函数(*)的基类的基类。 shared_ptr -like types can store a deleter suitable for deleting a shared_ptr<A> that points to a B even without virtual destructors. shared_ptr样类型可以存储适合于删除一个删除器shared_ptr<A>指向一个B即使没有虚拟析构函数。

(*) Although the current specification of std::unique_ptr doesn't contain such a check for the converting constructor template, it was restrained in an earlier draft, see LWG 854 . (*)尽管std::unique_ptr的当前规范不包含对转换构造函数模板的此类检查,但它在早期草案中受到限制,请参阅LWG 854 Proposal N3974 introduces the checked_delete deleter, which also requires a virtual dtor for derived-to-base conversions.提案N3974引入了checked_delete删除器,它还需要一个用于派生到基类转换的虚拟 dtor。 Basically, the idea is that you prevent conversions such as:基本上,这个想法是您防止转换,例如:

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

As N3974 suggests, this is a simple library extension;正如 N3974 所建议的,这是一个简单的库扩展; you can write your own version of checked_delete and combine it with std::unique_ptr .您可以编写自己的checked_delete版本并将其与std::unique_ptr结合使用。


Both suggestions in the OP can have performance drawbacks: OP 中的两个建议都可能存在性能缺陷:

  1. Mark a class as final将课程标记为final

This prevents the Empty Base Optimization.这可以防止空基优化。 If you have an empty class, its size must still be >= 1 byte.如果你有一个空类,它的大小仍然必须 >= 1 字节。 As a data member, it therefore occupies space.作为数据成员,它因此占用空间。 However, as a base class, it is allowed not to occupy a distinct region of memory of objects of the derived type.但是,作为基类,不允许占用派生类型对象的不同内存区域。 This is used eg to store allocators in StdLib containers.这用于例如在 StdLib 容器中存储分配器。 C++20 has mitigated this with the introduction of [[no_unique_address]] . C++20 通过引入[[no_unique_address]]缓解了这一点

  1. Have a virtual destructor有一个虚拟析构函数

If the class doesn't already have a vtable, this introduces a vtable per class plus a vptr per object (if the compiler cannot eliminate it entirely).如果类还没有 vtable,这会为每个类引入一个 vtable 加上每个对象一个 vptr(如果编译器不能完全消除它)。 Destruction of objects can become more expensive, which can have an impact eg because it's no longer trivially destructible.对象的破坏可能会变得更加昂贵,这可能会产生影响,例如因为它不再是微不足道的可破坏的。 Additionally, this prevents certain operations and restricts what can be done with that type: The lifetime of an object and its properties are linked to certain properties of the type such as trivially destructible.此外,这会阻止某些操作并限制可以使用该类型执行的操作:对象的生命周期及其属性与该类型的某些属性相关联,例如可简单破坏。


final prevents extensions of a class via inheritance. final防止通过继承扩展类。 While inheritance is typically one of the worst ways to extend an existing type (compared to free functions and aggregation), there are cases where inheritance is the most adequate solution.虽然继承通常是扩展现有类型的最糟糕的方法之一(与自由函数和聚合相比),但在某些情况下,继承是最合适的解决方案。 final restricts what can be done with the type; final限制了该类型可以做什么; there should be a very compelling and fundamental reason why I should do that.为什么应该这样做,应该有一个非常令人信服的根本原因 One cannot typically imagine the ways others want to use your type.人们通常无法想象其他人想要使用您的字体的方式。

TC points out an example from the StdLib: deriving from std::true_type and similarly, deriving from std::integral_constant (eg the placeholders). TC指出了一个来自 StdLib 的例子:派生自std::true_type ,类似地,派生自std::integral_constant (例如占位符)。 In metaprogramming, we're typically not concerned with polymorphism and dynamic storage duration.在元编程中,我们通常不关心多态性和动态存储持续时间。 Public inheritance often just the simplest way to implement metafunctions.公共继承通常只是实现元函数的最简单方法。 I do not know of any case where objects of metafunction type are allocated dynamically.我不知道动态分配元函数类型的对象的任何情况。 If those objects are created at all, it's typically for tag dispatching, where you'd use temporaries.如果完全创建了这些对象,则通常用于标记调度,您将在其中使用临时对象。


As an alternative, I'd suggest using a static analyser tool.作为替代方案,我建议使用静态分析器工具。 Whenever you derive publicly from a class without a virtual destructor, you could raise a warning of some sort.每当您从没有虚拟析构函数的类中公开派生时,您都可以发出某种警告。 Note that there are various cases where you'd still want to derive publicly from some base class without a virtual destructor;请注意,在许多情况下,您仍然希望从某个基类公开派生而没有虚拟析构函数; eg DRY or simply separation of concerns.例如 DRY 或简单的关注点分离。 In those cases, the static analyser can typically be adjusted via comments or pragmas to ignore this occurrence of deriving from a class w/o virtual dtor.在这些情况下,静态分析器通常可以通过注释或编译指示进行调整,以忽略这种从没有虚拟 dtor 的类派生的情况。 Of course, there need to be exceptions for external libraries such as the C++ Standard Library.当然,C++标准库等外部库需要有例外。

Even better, but more complicated is analysing when an object of class A w/o virtual dtor is deleted, where class B inherits from class A (the actual source of UB).更好,但更复杂的是分析何时删除了类A的对象( A虚拟 dtor),其中类B继承自类A (UB 的实际来源)。 This check is probably not reliable, though: The deletion can happen in a Translation Unit different to the TU where B is defined (to derive from A ).但是,此检查可能不可靠:删除可能发生在与定义B的 TU 不同的翻译单元中(从A派生)。 They can even be in separate libraries.它们甚至可以位于不同的库中。

The question that I usually ask myself, is whether an instance of the class may be deleted via its interface.我经常问自己的问题是,类的实例是否可以通过其接口删除。 If this is the case, I make it public and virtual.如果是这种情况,我会将其设为公开和虚拟的。 If this is not the case, I make it protected.如果不是这种情况,我会保护它。 A class only needs a virtual destructor if the destructor will be invoked through its interface polymorphically.如果析构函数将通过其接口以多态方式调用,则类只需要一个虚拟析构函数。

Well, to be strictly clear, it's only if the pointer is deleted or the object is destructed (through the base class pointer only) that the UB is invoked.好吧,严格来说,只有当指针被删除或对象被破坏(仅通过基类指针)时,才会调用 UB。

There could be some exceptions for cases where the API user cannot delete the object, but other than that, it's generally a wise rule to follow.对于 API 用户无法删除对象的情况,可能会有一些例外,但除此之外,遵循这一规则通常是明智的。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM