提问人:JMC 提问时间:9/1/2023 更新时间:9/4/2023 访问量:83
分配器的分配和构造是否通过 [basic.life]p8 明确定义?
Is allocators' allocate and construct well-defined through [basic.life]p8?
问:
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] 类型的数组,并开始其生存期,但不开始其任何元素的生存期。
并返回
[a pointer] 指向 N 个类型对象数组中的第一个元素,该数组的元素尚未构造。
Afaik,数组创建措辞已添加到标准中,以便指针上的指针算术有效。在任何情况下,这意味着返回的指针指向 的第一个元素,并且第一个元素的生存期未启动。T[]
construct()
然后在此位置创建一个对象,但是,它不会返回指向此对象的指针。我们唯一拥有的指针仍然是返回的指针。allocate
通常,当一个物体被放置在一个过期物体的位置时,它可以在[basic.life]p8中规定的条件下“透明地替换”旧的物体:(强调我的)
如果在对象的生存期结束后,在重用或释放该对象占用的存储之前,在原始对象占用的存储位置创建一个新对象,则指向原始对象的指针 [...] 将自动引用新对象,并且一旦新对象的生存期开始 [...]
此数组元素的生存期从未启动,因此它不可能结束,因此这不应适用。那么,如何保证访问新构造的对象是明确定义的呢?应该在每次通话后使用吗?std::launder
construct
请记住,这是一个带有 [language-lawyer] 标签的问题。这不是关于这个代码是否实际有效,而是关于标准的“定律”。
答:
我认为我们大多同意这段代码应该有效。 通常,仅当一个完整的对象被另一个相同类型的对象替换时才需要(忽略顶级 CV 限定符)1;[basic.life]/8.3.这里的基本原理是,如果编译器可以看到创建完整对象的代码,则可以假定指向该对象的指针或对该对象的引用是指向不变值的指针或引用,这提供了有用的优化机会。您必须使用它来禁用此优化。这表明,如果一开始就没有值(因为这是您第一次在特定存储区域中启动对象的生存期),则不需要(即使该对象是)。std::launder
const
const
std::launder
std::launder
const
我认为 OP 是正确的,标准中存在措辞差距。不幸的是,它目前可能不容易解决。问题在于,对于一个不在生命周期内的物体的身份,我们没有一个正式的特征。也许调用会创建一个具有特定标识 o1 的未初始化对象,并启动 o1 的生存期。如果是这种情况,那么就没有问题了:任何指向 o1 的指针仍然是指向 o1 的指针,除非指针通过赋值更改了其值。但这就引出了一个明显的后续问题。如果 o1 被销毁,并且一个新对象 o2 在同一个存储中开始其生命周期,我们知道 o1 和 o2 不是同一个对象,但是在 o1 的生命周期结束之后和 o2 的生命周期开始之前,我们能对存储说些什么呢?我们对使用指向该存储的指针或引用可以执行的操作进行了部分描述;[basic.life]/6-7。我们无法解释存储何时从“生命周期已结束的 o1”过渡到“生命周期尚未开始的 o2”占用。需要有人做找出答案并解决其所有含义的工作。(我怀疑没有一个答案具有所有期望的含义。allocate
construct
尽管如此,鉴于 OP 示例中的对象不是 even,实际上不可能最终解决这种歧义会使 OP 的代码具有未定义的行为。const
1 请注意,此类对象必须具有动态存储持续时间;[basic.life]/10.const
评论
launder
allocator::construct
使用的。new
launder
construct