分配器的分配和构造是通过[basic.life]p8明确定义的吗?

问题描述 投票:0回答:1

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';

对于分配者而言相当简单。然而,标准中的什么措辞保证

p1
实际上指向新对象?

根据 std::allocate[allocator.members] 上的 cppreference 文档, 默认分配器的

allocate()
函数

在存储中创建类型为 T[n] 的数组并开始其生命周期,但不启动其任何元素的生命周期。

并返回

[指针] 指向 n 个类型为 T 的对象数组的第一个元素,其元素尚未构造。

Afaik,数组创建措辞已添加到标准中,以便指针上的指针算术有效。无论如何,这意味着返回的指针指向

T[]
的第一个元素,并且该第一个元素的生命周期尚未开始。

construct()

然后在此位置创建一个对象,但是,它

返回指向该对象的指针。我们唯一的指针仍然是返回的那个 allocate
通常当一个对象被放置在过期对象的位置时,它可以在

[basic.life]p8

中规定的条件下“透明地替换”旧对象:(强调我的)

如果一个对象的生命周期结束后,在该对象占用的存储空间被重用或释放之前

,在原对象占用的存储位置创建了一个新对象,一个指向原对象的指针[ ...] 将自动引用新对象,并且一旦新对象的生命周期开始 [...]

这个数组元素的生命周期是
never

开始的,所以它不可能结束,所以这不应该适用。那么如何才能保证对新构造的对象的访问是明确定义的呢?每次 std::launder 调用后都应该使用

construct
吗?
请记住,这是一个带有[语言律师]标签的问题。问题不在于该代码是否实际有效,而在于标准的“法则”。

c++ memory-management language-lawyer allocator
1个回答
0
投票
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-7。我们没有任何解释存储空间何时从“o1,其生命周期已结束”占用转变为“o2,其生命周期尚未开始”。需要有人找出答案并解决它的所有含义。 (我怀疑没有一个答案能够具有所有期望的含义。)
尽管如此,鉴于 OP 示例中的对象甚至不是 

const

,最终解决这种歧义几乎不可能使 OP 的代码具有未定义的行为。

1

请注意,这样的 const 对象必须具有动态存储持续时间;

[基本生活]/10
.

© www.soinside.com 2019 - 2024. All rights reserved.