std::launder 的目的是什么?

What is the purpose of std::launder?

P0137 引入了函数模板 std::launder 并在有关联合、生命周期和指针的部分对标准进行了很多很多更改。

本文解决的问题是什么?我必须注意的语言有哪些变化?我们 launder 在做什么?

std::launder 的命名很恰当,但前提是您知道它的用途。它执行内存清洗

考虑论文中的例子:

struct X { const int n; };
union U { X x; float f; };
...

U u = {{ 1 }};

该语句执行聚合初始化,用 {1}.

初始化 U 的第一个成员

因为 n 是一个 const 变量,编译器可以自由假设 u.x.n 应该 总是 为 1.

如果我们这样做会发生什么:

X *p = new (&u.x) X {2};

因为 X 是微不足道的,我们不需要在创建新对象之前销毁旧对象,所以这是完全合法的代码。新对象的 n 成员将为 2.

那么告诉我... u.x.n return 会怎样?

显而易见的答案是 2。但这是错误的,因为允许编译器假设一个真正的 const 变量(不仅仅是 const&,而是一个对象变量 声明 const) 永远不会改变。但我们只是改变了它。

[basic.life]/8 阐明了可以通过 variables/pointers/references 访问新创建的对象到旧对象的情况。拥有 const 成员是取消资格的因素之一。

所以...我们如何正确地谈论 u.x.n

我们必须清洗我们的记忆:

assert(*std::launder(&u.x.n) == 2); //Will be true.

洗钱用于防止人们追踪您的资金来源。内存清洗用于防止 编译器 跟踪您从何处获取对象,从而迫使它避免任何可能不再适用的优化。

另一个不合格的因素是如果你改变了对象的类型。 std::launder 在这里也可以提供帮助:

alignas(int) char data[sizeof(int)];
new(&data) int;
int *p = std::launder(reinterpret_cast<int*>(&data));

[basic.life]/8告诉我们,如果你在旧对象的存储中分配一个新对象,你不能通过指向旧对象的指针访问新对象。 launder 允许我们回避这一点。

std::launder 是用词不当。此函数执行 相反 洗钱:它 土壤 指向的内存,以消除编译器可能对指向的值的任何期望.它排除了基于此类期望的任何编译器优化。

因此在@NicolBolas 的回答中,编译器可能会假设某些内存具有某些常量值;或未初始化。你是在告诉编译器:“那个地方(现在)很脏,不要做出那个假设”。

如果您想知道为什么编译器一开始总是坚持其幼稚的期望,并且需要您为它明显地弄脏东西 - 您可能想阅读此讨论:

...这让我想到了 std::launder 的含义。

我觉得std::launder有两个目的。

  1. 常量 folding/propagation 的障碍,包括 去虚拟化。
  2. 基于细粒度对象结构的别名分析的障碍。

过度攻击常数的障碍folding/propagation(已放弃)

从历史上看,C++ 标准允许编译器假定以某些方式获得的 const 限定或引用非静态数据成员的值是 immutable,即使它包含的对象是非const 并且可以通过 placement new 重用。

在 C++17/P0137R1, std::launder is introduced as a functionality that disables the aforementioned (mis-)optimization (CWG 1776), which is needed for std::optional. And as discussed in P0532R0 中,std::vectorstd::deque 的 table 实现也可能需要 std::launder,即使它们是 C ++98 个组件。

幸运的是,RU007 (included in P1971R0 和 C++20 禁止这种(错误的)优化。据我所知,没有编译器执行此(错误)优化。

去虚拟化的障碍

虚拟 table 指针 (vptr) 在其包含的多态对象的生命周期内可以被视为常量,这是去虚拟化所必需的。鉴于 vptr 不是非静态数据成员,编译器仍然允许基于 vptr 未更改的假设执行去虚拟化(即对象仍在其生命周期中,或者它被新对象重用相同的动态类型)在某些情况下。

对于一些用不同动态类型的新对象替换多态对象的不寻常用途(显示 here),需要 std::launder 作为去虚拟化的障碍。

IIUC Clang 使用这些语义 (LLVM-D40218) 实现了 std::launder (__builtin_launder)。

基于对象结构的别名分析的障碍

P0137R1 also changes the C++ object model by introducing pointer-interconvertibility. IIUC such change enables some "object-structure-based alias analysis" proposed in N4303.

因此,P0137R1 直接使用从未定义的 unsigned char [N] 数组取消引用 reinterpret_cast 的指针,即使该数组正在为另一个正确类型的对象提供存储。然后需要 std::launder 才能访问嵌套对象。

这种别名分析似乎过于激进,可能会破坏许多有用的代码库。据我所知,它目前还没有被任何编译器实现。

与基于类型的别名的关系analysis/strict 别名

IIUC std::launder 和基于类型的别名 analysis/strict 别名无关。 std::launder 要求正确类型的活动对象位于提供的地址。

不过,似乎在 Clang (LLVM-D47607) 中不小心将它们关联起来了。