首页 > 解决方案 > 在 std::launder 之前,std::vector 的所有实现都是不可移植的吗?

问题描述

在实例emplace_back()上调用时std::vector,会在先前分配的存储中创建一个对象。这可以通过完美便携的placement-new轻松实现。但是现在,我们需要在不调用未定义行为的情况下访问已放置的元素。

this SO post 我了解到有两种方法可以做到这一点

  1. 使用placement-new返回的指针: auto *elemPtr = new (bufferPtr) MyType();

  2. 或者,从 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])。但规则背后的意图并没有改变。

标签: c++c++11language-lawyerplacement-new

解决方案


在P0593R6P1971R0/RU007之后的 IIUC都合并到 C++20 中,并且作为针对先前版本的缺陷报告相当可观,便携式std::vector实现不需要std::launder.

首先,在allocate给定分配器的函数(可能调用operator newstd::malloc或类似的东西)返回后,在分配的存储中隐式创建一个value_type[N]数组(其中N等于传递给的请求数allocate)(感谢 P0593R6),因此指针算法是有效的。即使元素可能是非构造的。

其次,当我们使用placement-new withoutstd::launder时,构造的对象可以被视为数组元素,因为即使元素类型具有const/reference非静态数据成员,新对象也会透明地替换数组元素(感谢P1971R0/RU007及后续在P2103R0/US041中修复)。


推荐阅读