JMC*_*JMC 6 c++ memory-management allocator language-lawyer
cppreference 的 std::allocator 示例包含以下代码(为简单起见缩短):
// default allocator for ints
std::allocator<int> alloc1;
using traits_t1 = std::allocator_traits<decltype(alloc1)>; // The matching trait
p1 = traits_t1::allocate(alloc1, 1);
traits_t1::construct(alloc1, p1, 7); // construct the int
std::cout << *p1 << '\n';
Run Code Online (Sandbox Code Playgroud)
就分配器而言,相当简单。然而,标准保证中的哪些措辞p1实际上指向新对象呢?
根据std::allocate和[allocator.members]上的 cppreference 文档,默认分配器的函数allocate()
在存储中创建 T[n] 类型的数组并开始其生存期,但不开始其任何元素的生存期。
并返回
[指针] 指向 n 个 T 类型对象的数组的第一个元素,该数组的元素尚未构造。
Afaik,数组创建措辞已添加到标准中,以便指针上的指针算术有效。无论如何,这意味着返回的指针指向 的第一个元素,T[]并且该第一个元素的生命周期尚未开始。
construct()然后在此位置创建一个对象,但是,它不会返回指向该对象的指针。我们拥有的唯一指针仍然是返回的指针allocate。
通常,当一个对象被放置在过期对象的位置时,它可以在[basic.life]p8中规定的条件下“透明地替换”旧对象:(强调我的)
如果在一个对象的生命周期结束之后,在该对象所占用的存储被重用或释放之前,在原对象所占用的存储位置上创建了一个新对象,一个指向原对象的指针[... ] 将自动引用新对象,并且一旦新对象的生命周期开始[...]
该数组元素的生命周期从未开始,因此它不可能结束,因此这不适用。那么如何才能保证对新构造的对象的访问是明确定义的呢?每次通话后都std::launder应该使用吗construct?
请记住,这是一个带有[语言律师]标签的问题。问题不在于该代码是否实际有效,而在于该标准的“法律”。
我认为我们大多数人都同意这段代码应该可以工作。std::launder通常仅当一个完整的对象被另一个相同类型的对象替换时才需要const(忽略顶级 cv 限定符)1;[基本生活]/8.3。这里的基本原理是,如果编译器可以看到创建完整对象的代码const,则可以假设指向该对象的指针或对该对象的引用是指向不变值的指针或引用,这提供了有用的信息优化机会。您必须使用std::launder禁用此优化。这表明,如果一开始就没有值(因为这是您第一次在特定存储区域中启动对象的生命周期),那么std::launder就不需要(即使该对象是const)。
我认为OP是正确的,标准中存在措辞差距。不幸的是,目前这个问题可能不容易修复。问题是我们没有对不在其生命周期内的对象的身份进行正式的表征。也许该allocate调用创建了一个具有特定标识 o1 的未初始化对象,并且construct开始了 o1 的生命周期。如果是这种情况,那么就没有问题:任何指向 o1 的指针仍然是指向 o1 的指针,除非该指针通过分配而改变了其值。但这引出了一个明显的后续问题。如果 o1 被销毁,并且新对象 o2 在同一存储中开始其生命周期,我们知道 o1 和 o2 不是同一个对象,但是对于 o1 生命周期结束之后和 o2 之前的存储我们能说什么呢?的一生开始了吗?我们对使用该存储的指针或引用可以完成的操作有部分描述;[基本生活]/6\xe2\x80\x937。我们没有任何解释存储空间何时从“o1,其生命周期已结束”转换为“o2,其生命周期尚未开始”。需要有人找出答案并解决它的所有含义。(而且我怀疑没有一个答案能够具有所有期望的含义。)
尽管如此,鉴于 OP 示例中的对象不是 Even const,实际上解决这种歧义的最终解决方案不会使 OP 的代码具有未定义的行为。
1注意,这样的const对象必须有动态存储期限;[基本生活]/10。