c++ - 在 std::launder 之前,std::vector 的所有实现都是不可移植的吗?
问题描述
在实例emplace_back()
上调用时std::vector
,会在先前分配的存储中创建一个对象。这可以通过完美便携的placement-new轻松实现。但是现在,我们需要在不调用未定义行为的情况下访问已放置的元素。
从this SO post 我了解到有两种方法可以做到这一点
使用placement-new返回的指针:
auto *elemPtr = new (bufferPtr) MyType();
或者,从 C++17 开始,std::launder指针从
bufferPtr
auto *elemPtr2 = std::launder(reinterpret_cast<MyType*>(bufferPtr));
第二种方法可以很容易地推广到这样的情况,我们有很多对象放置在相邻的内存位置,如std::vector
. 但是人们在 C++17 之前做了什么?一种解决方案是将placement-new返回的指针存储在单独的动态数组中。虽然这当然是合法的,但我认为它并没有真正实现 std::vector [此外,单独存储我们已经知道的所有地址是一个疯狂的想法]。另一种解决方案是在lastEmplacedElemPtr
内部存储std::vector
,并从中删除一个适当的整数——但由于我们并没有真正的对象数组,MyType
这可能也是未定义的。事实上,来自这个 cppreference 页面的一个例子声称如果我们有两个相同类型的指针比较相等,并且其中一个可以安全地取消引用,那么取消引用另一个仍然可以是未定义的。
那么,有没有办法在 C++17 之前以可移植的方式实现 std::vector ?或者 std::launder 确实是 C++ 的关键部分,当涉及到新的放置时,自 C++98 以来就没有了?
我知道这个问题表面上与 SO 上的许多其他问题相似,但据我所知,他们都没有解释如何合法地迭代由placement-new 构造的对象。事实上,这有点令人困惑。例如,示例形式的cppreference 文档中的 std::aligned_storage
注释
似乎表明 C++11 和 C++17 之间发生了一些变化,并且reinterpret_cast
在 C++17 之前,简单的别名违规是合法的 [无需std::launder
]。同样,在文档 std::malloc
的示例中,
他们只是对返回的指针进行指针运算std::malloc
(在static_cast
正确类型之后)。
相比之下,根据这个 SO 问题的答案, 当涉及到placement-new 和时reinterpret_cast
:
自 C++11 以来,已经有一些重要的规则澄清(特别是 [basic.life])。但规则背后的意图并没有改变。
解决方案
在P0593R6和P1971R0/RU007之后的 IIUC都合并到 C++20 中,并且作为针对先前版本的缺陷报告相当可观,便携式std::vector
实现不需要std::launder
.
首先,在allocate
给定分配器的函数(可能调用operator new
、std::malloc
或类似的东西)返回后,在分配的存储中隐式创建一个value_type[N]
数组(其中N
等于传递给的请求数allocate
)(感谢 P0593R6),因此指针算法是有效的。即使元素可能是非构造的。
其次,当我们使用placement-new withoutstd::launder
时,构造的对象可以被视为数组元素,因为即使元素类型具有const/reference非静态数据成员,新对象也会透明地替换数组元素(感谢P1971R0/RU007及后续在P2103R0/US041中修复)。