为什么取消引用指向 std::set 的提取节点的指针是未定义的行为?

Why would dereferencing a pointer to an extracted node of std::set be undefined behaviour?

我在我的公司做了一个关于关联容器新的(C++17)拼接接口的闪电演讲。我演示了 std::set::extract 然后被问到迭代器和指向提取元素的指针会发生什么。他们让我措手不及,我无法回答这个问题,但在谈话后立即查了一下。

[associative.reqmts] 21.2.6.10在目前的标准草案中是这样写的:

The extract members invalidate only iterators to the removed element; pointers and references to the removed element remain valid. However, accessing the element through such pointers and references while the element is owned by a node_­type is undefined behavior. References and pointers to an element obtained while it is owned by a node_­type are invalidated if the element is successfully inserted.

(提案 P0083R3 已包含此措辞)

现在强调的部分真的让我很不安。我理解有效但不可取消引用的指针 (nullptr) 或迭代器(结束迭代器)的概念。我找到了 David Vandevoorde 的 "definition" of valid pointers 并了解到还有一些有效但不可取消引用的指针不是 nullptr。 (即指向现有对象的指针)

综上所述,我的心智模型如下:

  1. 检索到指向集合中元素的指针,因此该指针是有效的。取消引用该指针已定义并产生对集合中元素的访问。
  2. 现在从集合中提取相同的元素。从概念上讲,元素不会被复制、移动或以其他方式更改,它的关联节点只是从 set 管理其数据的内部树中删除。剩余的树可能需要重新平衡。返回的 node_handle 取得孤立树节点的所有权。

按照标准,在 1) 中检索到的指针仍然有效并且不能被 extract 更改,因此这也支持这种心智模型。然而,对于这个模型,没有理由取消引用指针会突然未定义。因此,在 coliru 上使用 g++ seems to work as I would have expected。 (这不是任何形式的证据)

标准给库实现者的回旋余地似乎不必要地大。我错过了什么?我只看到在提取它们时设置值的常量性被丢弃,但看不到这会有什么影响。

同样的推理适用于最后引用的句子中提到的插入案例。

您的心智模型忽略了移除常量的事实,严格来说,必须 是实现定义的。

node_handle 必须拥有一个 不同的 对象,但是通过一些实现定义的魔法,该可变对象在没有构造的情况下突然出现,具有相同的值和存储作为原始的 const 对象。

类似地,当它被插入到具有兼容分配器的集合中时,该集合会将 node_handle 拥有的可变对象变回原始的 const 对象。

这是未定义的行为,因为当 "it" 由 node_handle 拥有时,const 对象已不复存在,但当它被重新插入时它又开始存在。

如果您有 std::pair<const K, V>std::pair<K, V> 的用户定义特化,那么从映射中使用 node_handle 也是一种未定义的行为。您不想将实现限制在实现所有这些的 "magic" 方式上,因此您可以做任何会观察到 "magic" 未定义行为的事情。