c – 默认使类为“final”或给它们一个虚拟析构函数?

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

随着C 11加入最后一堂课,我想知道是否有必要设置以下规则:

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

>标记为final(如果它尚未(尚未)继承自)
>有一个虚拟析构函数(如果它是(或打算)从中继承)

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

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

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

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

虚拟析构函数也会影响释放函数的选择. vtable的存在也会影响type_id和dynamic_cast.

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

通过使用智能指针,您可以在许多情况下防止此特定问题:类似unique_ptr的类型可以将转换限制为具有虚拟析构函数(*)的基类的基类.类似shared_ptr的类型可以存储适合于删除shared_ptr< A>的删除器.即使没有虚拟析构函数,也指向B.

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

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个字节.因此,作为数据成员,它占据了空间.但是,作为基类,不允许占用派生类型的对象的不同内存区域.这用于例如在StdLib容器中存储分配器.

>有一个虚拟析构函数

如果该类还没有vtable,则每个类引入一个vtable加上每个对象的vptr(如果编译器无法完全消除它).物体的破坏会变得更加昂贵,这可能会产生影响,例如:因为它不再是简单的可破坏的.此外,这可以防止某些操作并限制对该类型可以执行的操作:对象的生命周期及其属性链接到该类型的某些属性,例如可以轻易破坏.

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

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

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

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

转载注明原文:c – 默认使类为“final”或给它们一个虚拟析构函数? - 代码日志