C++20 中的 std::launder 用例

Posted

技术标签:

【中文标题】C++20 中的 std::launder 用例【英文标题】:std::launder use cases in C++20 【发布时间】:2020-09-18 17:09:40 【问题描述】:

[1]

是否存在将p0593r6 添加到 C++20(§ 6.7.2.11 对象模型[intro.object])使得std::launder 不必要的情况,其中相同的用例在 C++17 中需要 std::launder,还是它们完全正交


[2]

[ptr::launder] 规范中的示例是:

struct X  int n; ;
const X *p = new const X3;
const int a = p->n;
new (const_cast<X*>(p)) const X5; // p does not point to new object ([basic.life]) because its type is const
const int b = p->n;                 // undefined behavior
const int c = std::launder(p)->n;   // OK

@Nicol Bolas in this SO answer 给出了另一个示例,使用指向有效存储但类型不同的指针:

aligned_storage<sizeof(int), alignof(int)>::type data;
new(&data) int;
int *p = std::launder(reinterpret_cast<int*>(&data));

是否还有其他用例,与允许转换两个不是transparently replaceable 的对象无关,以使用std::launder

具体来说:

reinterpret_cast 从 A* 到 B*,两者都是 pointer-interconvertible,在任何情况下都可能需要使用 std::launder? (即,两个指针是否可以 pointer-interconvertible 但不能透明地替换?规范在这两个术语之间没有关联)。 reinterpret_cast 从void* 到 T* 是否需要使用 std::launder? 下面的代码是否需要使用std::launder?如果是这样,规范中的哪种情况下需要这样做?

一个带有引用成员的结构,灵感来自this discussion:

struct A 
    constexpr A(int &x) : ref(x) 
    int &ref;
;

int main() 
    int n1 = 1, n2 = 2;
    A a  n1 ;
    a.~A();
    new (&a) A n2;
    a.ref = 3; // do we need to launder somebody here?
    std::cout << a.ref << ' ' << n1 << ' ' << n2 << std::endl;

【问题讨论】:

【参考方案1】:

在 C++17 之前,具有给定地址和类型的指针总是指向位于该地址的该类型的对象,前提是代码遵守 [basic.life] 的规则. (见:Is a pointer with the right address and type still always a valid pointer since C++17?)。

但在 C++17 标准中,为指针值添加了新特性。这种质量不是在指针类型中编码,而是直接限定值,与类型无关(这也是可追溯性的情况)。在[basic.compound]/3中有描述

指针类型的每个值都是以下之一:

指向对象或函数的指针(该指针被称为指向对象或函数),或

一个超过对象末尾的指针([expr.add]),或者

该类型的空指针值,或 一个无效的指针值。

指针值的这种性质有它自己的语义(转换规则),对于reinterpret_cast 的情况,在下一段中描述:

如果两个对象是指针可互转换的,那么它们具有相同的地址,并且可以通过reinterpret_cast。

在 [basic-life] 中,我们可以找到另一条规则来描述在重用对象存储时如何转换这种质量:

如果在一个对象的生命周期结束之后,在该对象占用的存储空间被重用或释放之前,在原对象占用的存储位置创建一个新的对象,一个指向该对象的指针原始对象、引用原始对象的引用或原始对象的名称将自动引用新对象,并且,[...]

如您所见,“指向对象的指针”质量附加到特定对象。

这意味着在您给出的第一个示例的以下变体中,reinterpret_cast 不允许我们不使用指针优化屏障:

struct X  int n; ;
const X *p = new const X3;
const int a = p->n;
new (const_cast<X*>(p)) const X5; // p does not point to new object ([basic.life]) because its type is const
const int b = *reinterpret_cast <int*> (p);                 // undefined behavior
const int c = *std::launder(reinterpret_cast <int*> (p)); 

reinterpret_cast 不是指针优化屏障:reinterpret_cast &lt;int*&gt;(p) 指向被销毁对象的成员。

另一种设想它的方法是,只要对象是指针可相互转换的,或者如果它被强制转换为 void 然后返回到指针可相互转换的类型,reinterpret_cast 就可以保留“指向”质量。 (见 [exp.static_cast]/13)。所以reinterpret_cast &lt;int*&gt;(reinterpret_cast &lt;void*&gt;(p)) 仍然指向被破坏的对象。

对于您给出的最后一个示例,名称 a 指的是一个非 const 完整对象,因此原始 a 可以透明地被新对象替换。


对于第一个问题,您会问:“在任何情况下,将 p0593r6 添加到 C++20(第 6.7.2.11 节对象模型 [intro.object])中会产生 std::不需要清洗,C++17 中的相同用例需要 std::launder,还是它们完全正交?"

老实说,我还没有找到任何 std::launder 可以补偿隐含生命周期对象的情况。但我发现一个例子是隐式生命周期对象使 std::launder 有用:

  class my_buffer 
      alignas(int) std::byte buffer [2*sizeof(int)];
      
      int * begin()
         //implictly created array of int inside the buffer
         //nevertheless to get a pointer to this array, 
         //std::launder is necessary as the buffer is not
         //pointer inconvertible with that array
         return *std::launder (reinterpret_cast <int(*)[2]>(&buffer));
         
      create_int(std::size_t index, int value)
         new (begin()+index) autovalue;
         
       ;
      

【讨论】:

“正如您所见,“指向对象的指针”的质量附加到特定对象” [basic.life] 中的这条规则在 C++17 之前就存在。所以这种品质并不新鲜。 @LanguageLawyer 我同意这一点。 虽然答案提供了对该主题的最彻底的解释,但它并没有直接引用具体问题。 @AmirKirsh 你的意思是前两个问题,不是吗? 有几个问题没有回答:p0593和std::launder正交吗?两个指针可以指针互转换但不能透明替换吗?从 void* 到 T* 的 reinterpret_cast 是否需要使用 std::launder?是否存在与允许投射两个不可透明替换的对象无关的用例,用于使用 std::launder?

以上是关于C++20 中的 std::launder 用例的主要内容,如果未能解决你的问题,请参考以下文章

为啥要引入 `std::launder` 而不是让编译器处理它?

为啥这里需要 std::launder ?

我在哪里可以找到 std::launder 的真正作用? [复制]

如何解释 std::launder 的可达性要求?

带有就地多态容器的 std::launder

std::launder 的效果是不是在调用它的表达式之后持续?