std::optional<>::emplace() 是否会使对内部值的引用无效?
Does std::optional<>::emplace() invalidate references to the inner value?
考虑以下片段(假设 T
是平凡可构造且平凡可破坏的):
std::optional<T> opt;
opt.emplace();
T& ref = opt.value();
opt.emplace();
// is ref guaranteed to be valid here?
从 the definition of std::optional
我们知道包含的实例保证在 std::optional
容器内分配,因此我们知道引用 ref
将始终引用相同的内存地点。是否存在指向对象被销毁并重新构造后该引用将不再有效的情况?
C++20 有以下规则,[basic.life]/8:
If, after the lifetime of an object has ended and before the storage which the object occupied is reused or released, a new object is created at the storage location which the original object occupied, a pointer that pointed to the original object, a reference that referred to the original object, or the name of the original object will automatically refer to the new object and, once the lifetime of the new object has started, can be used to manipulate the new object, if the original object is transparently replaceable (see below) by the new object. An object o1 is transparently replaceable by an object o2 if:
- the storage that o2 occupies exactly overlays the storage that o1 occupied, and
- o1 and o2 are of the same type (ignoring the top-level cv-qualifiers), and
- o1 is not a complete const object, and
- neither o1 nor o2 is a potentially-overlapping subobject (6.7.2), and
- either o1 and o2 are both complete objects, or o1 and o2 are direct subobjects of objects p1 and p2 ,
respectively, and p1 is transparently replaceable by p2.
这表明只要 T
不是 const-qualified,破坏 std::optional<T>
中的 T
然后重新创建它应该会导致对旧的引用对象自动引用新对象。正如评论部分所指出的,这是对旧行为的更改,取消了 T
不得包含 const-qualified 或引用类型的 non-static 数据成员的要求。 (编辑:我之前断言该更改是追溯性的,因为我将其与 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()
。最后,为了构造新的 T
对象,调用类似于 construct_at(addressof(object))
的东西。
并不是说任何实现都会这样做,但您可以想象一个实现,在 对 object.~T()
和 construct_at(addressof(object))
的调用之间,re-initializes no_object
成员。这将是对先前被 object
占用的存储空间的“重用”。这意味着不满足 [basic.life]/8 的要求。
当然,您问题的实际答案是:(1) 没有理由让实现做这样的事情,并且 (2) 即使实现做了,开发人员也会确保您的代码仍然表现得好像 T
对象被透明地替换了。在标准库实现合理的假设下,您的代码是合理的,并且编译器开发人员不喜欢用它来破坏代码 属性,因为这样做会不必要地激怒他们的用户。
但是如果编译器开发人员倾向于破坏您的代码(基于未定义行为越多,编译器可以优化的越多的论点)那么他们甚至可以破坏您的代码 而无需 更改 <optional>
头文件。用户需要将标准库视为一个“黑匣子”,它只保证标准明确保证的内容。因此,在对标准的迂腐阅读下,未指定 是否在第二次 emplace
调用具有未定义行为后尝试访问 ref
。如果未指定它是否是 UB,则允许编译器在需要时开始将其视为 UB。
1 这是历史原因; C++17 要求 constexpr
构造函数只初始化联合的一个变体成员。此规则在 C++20 中被废除,因此 C++20 实现可以省略 no_object
成员。
考虑以下片段(假设 T
是平凡可构造且平凡可破坏的):
std::optional<T> opt;
opt.emplace();
T& ref = opt.value();
opt.emplace();
// is ref guaranteed to be valid here?
从 the definition of std::optional
我们知道包含的实例保证在 std::optional
容器内分配,因此我们知道引用 ref
将始终引用相同的内存地点。是否存在指向对象被销毁并重新构造后该引用将不再有效的情况?
C++20 有以下规则,[basic.life]/8:
If, after the lifetime of an object has ended and before the storage which the object occupied is reused or released, a new object is created at the storage location which the original object occupied, a pointer that pointed to the original object, a reference that referred to the original object, or the name of the original object will automatically refer to the new object and, once the lifetime of the new object has started, can be used to manipulate the new object, if the original object is transparently replaceable (see below) by the new object. An object o1 is transparently replaceable by an object o2 if:
- the storage that o2 occupies exactly overlays the storage that o1 occupied, and
- o1 and o2 are of the same type (ignoring the top-level cv-qualifiers), and
- o1 is not a complete const object, and
- neither o1 nor o2 is a potentially-overlapping subobject (6.7.2), and
- either o1 and o2 are both complete objects, or o1 and o2 are direct subobjects of objects p1 and p2 , respectively, and p1 is transparently replaceable by p2.
这表明只要 T
不是 const-qualified,破坏 std::optional<T>
中的 T
然后重新创建它应该会导致对旧的引用对象自动引用新对象。正如评论部分所指出的,这是对旧行为的更改,取消了 T
不得包含 const-qualified 或引用类型的 non-static 数据成员的要求。 (编辑:我之前断言该更改是追溯性的,因为我将其与 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()
。最后,为了构造新的 T
对象,调用类似于 construct_at(addressof(object))
的东西。
并不是说任何实现都会这样做,但您可以想象一个实现,在 对 object.~T()
和 construct_at(addressof(object))
的调用之间,re-initializes no_object
成员。这将是对先前被 object
占用的存储空间的“重用”。这意味着不满足 [basic.life]/8 的要求。
当然,您问题的实际答案是:(1) 没有理由让实现做这样的事情,并且 (2) 即使实现做了,开发人员也会确保您的代码仍然表现得好像 T
对象被透明地替换了。在标准库实现合理的假设下,您的代码是合理的,并且编译器开发人员不喜欢用它来破坏代码 属性,因为这样做会不必要地激怒他们的用户。
但是如果编译器开发人员倾向于破坏您的代码(基于未定义行为越多,编译器可以优化的越多的论点)那么他们甚至可以破坏您的代码 而无需 更改 <optional>
头文件。用户需要将标准库视为一个“黑匣子”,它只保证标准明确保证的内容。因此,在对标准的迂腐阅读下,未指定 是否在第二次 emplace
调用具有未定义行为后尝试访问 ref
。如果未指定它是否是 UB,则允许编译器在需要时开始将其视为 UB。
1 这是历史原因; C++17 要求 constexpr
构造函数只初始化联合的一个变体成员。此规则在 C++20 中被废除,因此 C++20 实现可以省略 no_object
成员。