std::optional<>::emplace() 是否会使对内部值的引用无效?



考虑以下片段(假设T是平凡可构造的和平凡可破坏的):

std::optional<T> opt;
opt.emplace();
T& ref = opt.value();
opt.emplace();
// is ref guaranteed to be valid here?

根据std::optional的定义,我们知道所包含的实例保证分配在std::optional容器内,因此我们知道引用ref将始终引用相同的内存位置。是否存在在所指向的对象被破坏然后再次构建后,所述引用将不保持有效性的情况?

C++20有以下规则,[basic.life]/8:

如果在对象的生存期结束后,在对象所占用的存储被重用或释放之前,在原始对象所占据的存储位置创建了一个新对象,则指向原始对象的指针、引用原始对象的引用或原始对象的名称将自动引用新对象,一旦新对象的生存期开始,如果原始对象可以被新对象透明地替换(见下文),则可以用于操作新对象。对象o1可由对象o2透明替换,如果:

  • o 2占用的存储正好覆盖o<1>所占用的存储,并且
  • o 1o2属于相同类型(忽略顶级cv限定符),并且
  • o 1不是一个完整的常量对象,并且
  • o 1o2都不是潜在的重叠子对象(6.7.2),并且
  • o 1o2都是完整对象,或者o<1>与o<2>是对象p<1>及p<2>1可被p2透明地替换

这表明,只要T不是常量限定的,破坏std::optional<T>内部的T,然后重新创建它,就会导致对旧对象的引用自动引用新对象。正如评论部分所指出的,这是对旧行为的改变,取消了T不得包含const限定或引用类型的非静态数据成员的要求。(编辑:我之前断言这个更改是追溯性的,因为我把它与C++20中的另一个更改混淆了。我不确定N4858中所示的RU 007和US 042的决议是否具有追溯力,但我怀疑答案是肯定的,因为需要进行更改来修复涉及标准库模板的代码,这些模板可能不是从C++11到C++17的。)

然而,我们假设正在创建新的T对象";在[旧]对象占用的存储器被重新使用或释放之前";。如果我正在写一篇";对抗性的";实现标准库时,我可以设置它,以便emplace调用在创建新的T对象之前重用底层存储。这将防止旧的T对象被新的对象透明地替换。

如何实现";再利用";存储?通常,底层存储可以这样声明:

union {
char no_object;
T object;
};

当调用optional的默认构造函数时,no_object被初始化(该值无关紧要)1emplace()调用检查是否存在T对象(通过检查此处未示出的标志)。如果存在T对象,则调用object.~T()。最后,调用类似于construct_at(addressof(object))的东西来构造新的T对象。

并不是说任何实现都会这样做,但您可以想象一个实现,在对object.~T()construct_at(addressof(object))的调用之间,会重新初始化no_object成员。这将是一个";再利用";CCD_ 24先前占用的存储器的大小。这意味着没有满足[基本生活]/8的要求。

当然,您的问题的实际答案是:(1)实现没有理由做这样的事情,(2)即使实现做到了,开发人员也会确保您的代码仍然像T对象被透明地替换一样。在标准库实现是合理的假设下,您的代码是合理的,编译器开发人员不喜欢使用该属性破坏代码,因为这样做会不必要地激怒用户。

但是,如果编译器开发人员倾向于破坏你的代码(基于这样一种论点,即未定义的行为越多,编译器就可以优化得越多),那么他们甚至可以在不更改<optional>头文件的情况下破坏代码。要求用户将标准库视为"标准库";黑盒";这只能保证标准明确保证的内容。因此,根据对标准的迂腐解读,在第二次emplace调用后尝试访问ref是否有未定义的行为,这是未指定的。如果未指定是否为UB,则编译器可以随时将其视为UB。

1这是历史原因;C++17要求constexpr构造函数只初始化并集的一个变体成员。该规则在C++20中被废除,因此C++20实现可以省略no_object成员。

最新更新