如果将具有非虚拟析构函数的类用作基类(如果使用指向基类的指针或引用引用子类的实例),则这些类是错误的源。
在C++ 11中添加了EDCOX1和0的类,我想知道下面的规则是否成立是有意义的:
每个类必须满足以下两个属性之一:
标记为final(如果还没有打算继承)
有一个虚拟析构函数(如果它是(或打算)继承自)
可能有两种选择都不合理的情况,但我想它们可以作为例外处理,应该仔细记录。
- 不是每个继承层次都需要virtual性。
- 真的。类型特征类通常甚至没有实例化,因此也不需要销毁它们。所以第三种允许的情况是"没有构造函数"。
型
我经常问自己的问题是,是否可以通过类的接口删除该类的实例。如果是这样的话,我将它公开化和虚拟化。如果不是这样,我会保护它。如果通过接口多态地调用析构函数,则类只需要一个虚拟析构函数。
- 型我现在找到了Herb Sutter的这篇文章,这篇文章更详细。你的答案基本上是它的总结(或者更确切地说是它的第二部分):gotw.ca/publications/mill18.htm。
- 型读《药草经》(很久以前)无疑影响了我的想法是的。现在这种想法已经成为我的一部分了。
- 现在我觉得这个答案是最好的建议,表明我最初的问题并不完全合理。我正在考虑把这个问题作为公认的答案,但是@dyp更直接地回答了最初的问题,所以我不确定这是否是正确的做法。
- 我觉得@dyp的回答值得(而且信息丰富)作为公认的回答(没有异议)。你可以编辑你的帖子,指出这个问题并不完全合理,如果你愿意的话,可以参考我的回答。
由于缺少虚拟析构函数,最常见的实际问题可能是通过指向基类的指针删除对象:好的。
1 2 3 4 5
| 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类型可以存储适合删除指向B的shared_ptr的删除程序,即使没有虚拟析构函数。好的。
(*)虽然当前的std::unique_ptr规范没有包含转换构造函数模板的检查,但它在早期的草案中受到了限制,请参见LWG854。提案N3974介绍了checked_delete删除程序,它还需要一个虚拟DTOR来进行派生到基的转换。基本上,您可以防止转换,例如:好的。
1 2 3 4
| 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中的两个建议都可能存在性能缺陷:好的。
将一个类标记为final。
这将阻止空基优化。如果有空类,则其大小必须大于等于1字节。因此,作为数据成员,它占用空间。但是,作为一个基类,它不允许占用派生类型对象的独特内存区域。这用于将分配器存储在stdlib容器中。好的。
有一个虚拟析构函数
如果类还没有vtable,这将为每个类引入vtable,并为每个对象添加vptr(如果编译器不能完全消除它)。对物体的破坏会变得更加昂贵,这会产生影响,例如,因为它不再是微不足道的可破坏性。此外,这可以防止某些操作,并限制可以对该类型执行的操作:对象的生存期及其属性链接到该类型的某些属性,如可销毁的属性。好的。
final防止通过继承来扩展类。虽然继承通常是扩展现有类型的最糟糕方法之一(与自由函数和聚合相比),但在某些情况下继承是最合适的解决方案。final限制了该类型可以做什么;应该有一个非常有说服力和根本性的原因,我应该这样做。人们通常无法想象其他人使用您的类型的方式。好的。
T.C.指出了stdlib的一个例子:源自std::true_type,类似地,源自std::integral_constant(例如占位符)。在元编程中,我们通常不关心多态性和动态存储时间。公共继承通常只是实现元函数的最简单方法。我不知道任何情况下元函数类型的对象是动态分配的。如果这些对象是完全创建的,那么它通常用于标记调度,在这里您将使用临时的。好的。
作为替代方案,我建议使用静态分析仪工具。无论何时从没有虚拟析构函数的类中公开派生,都可以引发某种类型的警告。请注意,在许多情况下,您仍然希望在没有虚拟析构函数的情况下从一些基类公开派生;例如,干性或简单地分离关注点。在这些情况下,静态分析器通常可以通过注释或pragma进行调整,以忽略从类w/o虚拟dtor派生的这种情况。当然,对于外部库(如C++标准库),需要有例外。好的。
如果删除了类A的一个对象,而类B是从类A继承来的,那么分析起来会更好,但更复杂。不过,这种检查可能不可靠:删除可能发生在与定义B的tu不同的翻译单元中(从A派生)。它们甚至可以在单独的库中。好的。好啊。
- 型我希望这样一个静态分析仪至少可以被教导忽略来自std::true_type和std::false_type的推导。
- 型@T.C.在大多数情况下,派生问题仅限于new/delete(即使.~T()可能发生在非自由存储数据上,如果您手动使用析构函数,那么您可能知道自己在做什么)。这些类型可以标记为"动态分配不安全",并在您(非放置)new X时发出警告?
- 型我对你首先谈论绩效的态度非常失望,因为这是一个重要的问题。
- 型@小狗,这是唯一可以衡量的问题。OP是在谈论一个一般规则。上一次我看到libstdc++甚至不支持final分配器,因为它们总是尝试使用ebo。--其他缺点本质上是缺点,可以通过更多的努力来克服(例如私有继承+使用声明、聚合等)。交易"更多的努力"和"更安全的代码"通常是公司/团队/产品决策。毕竟,OP中的两个选项都排除了一种错误。
- 型能够测量它是没用的,因为这是一个问题。
- 型@在这种情况下,我可能不明白你为什么说"无问题"。你认为它不会对一个真正的程序的性能产生重大影响吗?
- 型你能详细解释一下你的评论吗"通过使用智能指针,你可以在很多情况下避免这个特殊的问题:独特的类类型可以限制转换为具有虚拟析构函数的基类的基类"。这在实践中意味着什么?如果我把一个Derived指针放到一个std::unique_ptr上,它似乎不会改变任何东西。这是关于未来变化的建议吗?
- 型@Simon当前的std::unique_ptr规范不需要(也不允许)这样的检查。但是,您可以轻松地编写自己的checked_delete删除程序,并将其与std::unique_ptr结合起来。要么跟着N3974走,要么检查一下有没有像is_convertible::value && (is_same,remove_cv_t>::value || not is_class::value || has_virtual_destructor::value)这样的东西。
- 型实际上,这应该是is_convertible::value && (not is_strict_base_of::value || has_virtual_destructor::value)--也就是说,只有在派生->基的情况下,我们才需要强制执行虚拟DTOR。其中is_same, remove_cv_t>::value表示false == is_strict_base_of::value。
型
好吧,严格地说,只有当指针被删除或对象被破坏(仅通过基类指针)时,才会调用UB。
在API用户不能删除对象的情况下,可能会有一些例外,但除此之外,这通常是一个明智的规则。
- 型不是每个对象都会在某个时刻被删除/销毁,最迟是在程序终止时(除非不进行清理)?所以我不知道你的第一句话是什么意思。
- 型@Simon:UB发生在使用指向基类的指针删除了一个不容易销毁的对象,并且基类型的析构函数不是虚拟的情况下。你说每一个对象都被销毁了,这是真的,除非它被泄露了,但这并不意味着它是用指向基类的指针删除的。只要你以正确的方式摧毁它就没问题。还要注意,即使所有对象都被销毁,也不一定使用delete来销毁它们。
- 型@SteveJessop琐碎的析构函数并不重要;如果静态类型与动态类型不同,并且没有虚拟析构函数,那么不管析构函数是琐碎的,它都是ub。
- 型@T.C.:我不认为这是正确的,我认为它说如果动态类型的析构函数是微不足道的,那么你就可以了。但我现在不能检查,所以你可能是对的。
- 型@stevejessop stackoverflow.com/questions/29841845/&hellip;