默认使类为`final`或给他们一个虚拟析构函数? [英] Default to making classes either `final` or give them a virtual destructor?

查看:240
本文介绍了默认使类为`final`或给他们一个虚拟析构函数?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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



使用C ++ 11添加 final 类,我想知道是否有必要设置以下规则: / p>

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


  1. c $ c> final (如果它不是要继承自)

  2. 有一个虚拟析构函数)可以继承自)

可能有两种情况都不是有意义的,但我想他们可以被视为异常

解决方案

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

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

Base * b = new Derived();
delete b; //未定义的行为

虚拟析构函数也会影响解除分配函数的选择。 vtable的存在还影响 type_id dynamic_cast



如果你的类不以这些方式使用,那么对于虚拟析构函数没有需要。请注意,此用法是不是类型的属性,既不是类型 Base 也不是类型 Derived 。继承使得这样的错误成为可能,而只使用隐式转换。 (使用显式转换,例如 reinterpret_cast ,类似的问题可能没有继承。)



通过使用智能指针在许多情况下可以防止这个特殊的问题: unique_ptr - 类型可以限制转换到一个基类的基类有一个虚拟析构函数(*) shared_ptr 类似类型可以存储适合删除 shared_ptr 删除程序



(*)虽然目前的规范 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)); //错误

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






OP 中的两个建议都有性能上的缺点:


  1. 将类标记为 final

防止空基本优化。如果你有一个空类,它的大小仍然必须> = 1个字节。因此作为数据成员,它占据空间。然而,作为基类,允许不占据派生类型的对象的不同区域的存储器。这用于例如。


  1. 有一个虚拟析构函数

如果类没有vtable,这将引入一个vtable每个类加一个vptr每个对象(如果编译器不能完全消除)。对象的破坏可能变得更昂贵,这可能具有影响。因为它不再是可耻的。此外,这防止了某些操作,并限制了该类型可以做什么:对象及其属性的生命周期链接到类型的某些属性,例如可破坏的。



< hr>

final 阻止类通过继承扩展。虽然继承通常是扩展现有类型(与自由函数和聚合相比)的最糟糕的方法之一,但有时继承是最适当的解决方案。 final 限制了可以对类型执行的操作;应该有一个非常引人注目和基本的原因为什么应该这样做。人们通常无法想象他人想要如何使用您的类型。



TC 从StdLib指出一个例子:从 std :: true_type 导出,类似地,从 std :: integral_constant (例如占位符)。在元编程中,我们通常不关心多态性和动态存储持续时间。公共继承往往只是实现元函数的最简单的方法。我不知道动态分配元功能类型的对象的任何情况。






另一种方法是使用临时实体,我建议使用静态分析工具。每当您从没有虚拟析构函数的类中公开派生 时,您可以提出某种警告。请注意,有很多情况下,你仍然想从一些没有虚拟析构函数的基类中公开派生;例如DRY或简单地分离关注点。在这些情况下,通常可以通过注释或pragmas来调整静态分析器以忽略来自类w / o虚拟dtor的这种发生。当然,外部库(如C ++标准库)也需要例外。



更好,但更复杂的是分析类 A w / o虚拟dtor被删除,其中 B 类继承自 A (UB的实际来源)。但是这种检查可能不可靠:删除可以发生在不同于TU的翻译单元中,其中 B 被定义(从 / code>)。他们甚至可以在单独的库中。


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:

Every class must fulfil one of these two properties:

  1. be marked final (if it is not (yet) intended to be inherited from)
  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.

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. 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.)

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 (*). shared_ptr-like types can store a deleter suitable for deleting a shared_ptr<A> that points to a B even without virtual destructors.

(*) 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. Proposal N3974 introduces the checked_delete deleter, which also requires a virtual dtor for derived-to-base conversions. 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; you can write your own version of checked_delete and combine it with std::unique_ptr.


Both suggestions in the OP can have performance drawbacks:

  1. Mark a class as final

This prevents the Empty Base Optimization. If you have an empty class, its size must still be >= 1 byte. 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 e.g. to store allocators in StdLib containers.

  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). Destruction of objects can become more expensive, which can have an impact e.g. 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. 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; 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.

T.C. points out an example from the StdLib: deriving from std::true_type and similarly, deriving from std::integral_constant (e.g. the placeholders). 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; e.g. DRY or simply separation of concerns. 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. Of course, there need to be exceptions for external libraries such as the C++ Standard Library.

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). 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). They can even be in separate libraries.

这篇关于默认使类为`final`或给他们一个虚拟析构函数?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆