Har*_*ger 22 c++ destructor final vector
在向量中,std::vector<T>向量拥有分配的存储空间,它构造Ts 并析构Ts。无论 的T类层次结构如何,std::vector<T>都知道它只创建了 a T,因此在.pop_back()调用时它只需要销毁 a T(而不是 的某些派生类T)。采取以下代码:
#include <vector>
struct Bar {
virtual ~Bar() noexcept = default;
};
struct FooOpen : Bar {
int a;
};
struct FooFinal final : Bar {
int a;
};
void popEm(std::vector<FooOpen>& v) {
v.pop_back();
}
void popEm(std::vector<FooFinal>& v) {
v.pop_back();
}
Run Code Online (Sandbox Code Playgroud)
https://godbolt.org/z/G5ceGe6rq
for只是将向量的大小减少 1(元素)PopEm。FooFinal这是有道理的。但是PopEmforFooOpen调用该类通过扩展而获得的虚拟析构函数Bar。鉴于这FooOpen不是最终的,如果在指针delete fooOpen上调FooOpen*用法线,它将需要执行虚拟析构函数,但在这种情况下,std::vector它知道它只创建了一个FooOpen并且没有构造它的派生类。因此,不能std::vector<FooOpen>将类视为最终类并省略对虚拟析构函数的调用吗pop_back()?
Bar*_*uza 12
长话短说 - 编译器没有足够的上下文信息来推断它https://godbolt.org/z/roq7sYdvT
无聊部分:
所有 3 个的结果都是相似的:msvc、clang 和 gcc,所以我猜这个问题是普遍存在的。我分析了 libstdc++ 代码只是为了发现 pop_back() 运行如下:
void pop_back() // a bit more convoluted but boils-down to this
{
--back;
back->~T();
}
Run Code Online (Sandbox Code Playgroud)
没什么意外的。就像C++教科书上的那样。但它显示了问题 - 从指针对析构函数的虚拟调用。我们正在寻找的是此处描述的“去虚拟化”技术:最终用于 C++ 中的优化- 它指出去虚拟化是“假设”行为,因此如果编译器有足够的信息,它看起来就可以进行优化做吧。
我的想法:
我对代码进行了一些修改,我认为优化不会发生,因为编译器无法推断出“back”指向的唯一对象是 FooOpen 实例。我们——人类——知道这一点,因为我们分析了整个类,并了解了将元素存储在向量中的总体概念。我们知道指针必须仅指向 FooOpen 实例,但编译器无法看到它 - 它只看到一个可以指向任何地方的指针(向量分配未初始化的内存块,其解释是向量逻辑的一部分,指针也在外部修改pop_back() 的范围)。在不知道 vector<> 的整个概念的情况下,我不认为如何推断出它(不分析整个类)它不会指向可以在其他翻译单元中定义的 FooOpen 的任何后代。
FooFinal 不存在这个问题,因为它已经保证没有其他类可以继承它,因此对于 FooFinal* 或 FooFinal& 指向的对象来说,去虚拟化是安全的。
更新 我做了一些可能有用的发现:
https://godbolt.org/z/3a1bvax4o),只要不涉及指针算术,非最终类就可以发生去虚拟化。
https://godbolt.org/z/xTdshfK7v std::array 对非最终类执行去虚拟化。即使 std::vector 在同一范围内构造和销毁,它也无法执行此操作。
https://godbolt.org/z/GvoaKc9Kz可以使用包装器启用去虚拟化。
https://godbolt.org/z/bTosvG658析构函数去虚拟化可以通过分配器启用。有点 hacky,但对用户来说是透明的。简要测试了一下。
是的,这是一个错过的优化。
请记住,编译器是一个软件项目,必须编写功能才能存在。在这种情况下,虚拟销毁的相对开销可能足够低,因此到目前为止,添加此功能还不是 gcc 团队的优先事项。
这是一个开源项目,因此您可以提交一个补丁来添加此项目。