考虑以下片段(假设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 1和o2属于相同类型(忽略顶级cv限定符),并且
- o 1不是一个完整的常量对象,并且
- o 1和o2都不是潜在的重叠子对象(6.7.2),并且
- o 1和o2都是完整对象,或者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
被初始化(该值无关紧要)1。emplace()
调用检查是否存在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
成员。