std :: launder的目的是什么?

Bar*_*rry 223 c++ memory c++-faq c++17 stdlaunder

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

这篇论文解决了什么问题?我必须注意哪些语言的变化?我们在做什么launder

Nic*_*las 232

std::launder虽然只是你知道它的用途,但它的名字恰如其分.它执行内存清洗.

考虑一下文中的例子:

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

U u = {{ 1 }};
Run Code Online (Sandbox Code Playgroud)

该语句执行聚合初始化,初始化Uwith 的第一个成员{1}.

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

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

X *p = new (&u.x) X {2};
Run Code Online (Sandbox Code Playgroud)

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

那么告诉我......会有什么u.x.n回报?

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

[basic.life]/8说明了通过变量/指针/对旧对象的引用来访问新创建的对象的情况.拥有const会员是不合格的因素之一.

那么......我们怎么能u.x.n正确谈论呢?

我们必须清洗记忆:

assert(*std::launder(&u.x.n) == 2); //Will be true.
Run Code Online (Sandbox Code Playgroud)

洗钱用于防止人们从您从中获取资金的地方进行追踪.内存清洗用于防止编译器跟踪从中获取对象的位置,从而强制它避免任何可能不再适用的优化.

另一个不合格的因素是你改变了对象的类型.std::launder也可以在这里帮忙:

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

[basic.life]/8告诉我们,如果在旧的存储中分配一个新对象,则无法通过指向旧对象的方式访问新对象.launder允许我们支持这一点.

  • 所以我的tl;博士是正确的:"洗钱基本上是针对非UB类型的惩罚"? (31认同)
  • @NicolBolas人们只能希望supercat对委员会进行尽可能多的游说,因为他们无休地要求SO上的其他第三方语言用户提供答案.此外,一个优秀的优化编译器将优化你的正确解决方案`memcpy`到支持(即松散对齐)平台_anyway_的就地重新解释. (15认同)
  • @Barry很好; 如果没有类型T的对象位于地址`ptr`表示,那么你打破了'washder'的前提条件,所以没有必要谈论结果. (12认同)
  • 你能解释为什么这是真的吗?*"因为`n`是一个`const`变量,编译器可以自由地假设`uxn`应该始终为1."*标准中的哪个位置表示?我问,因为你指出的问题似乎意味着我首先是假的.它应该只在as-if规则下才是真的,这在这里失败了.我错过了什么? (11认同)
  • 我们可以避开多少锯齿规则?就像`template <class T,class U> T*alias_cast(U*ptr){return std :: launder(reinterpret_cast <T*>(ptr)); 那UB怎么样? (9认同)
  • @Mehrdad [basic.life]/8:"*如果,[...]在原始对象占用的存储位置创建一个新对象[...]原始对象的名称将自动引用新对象object [...] if:[...]类型[...]不包含任何类型为const限定的非静态数据成员或引用类型[...]*" (7认同)
  • `&u.x`是指向const int的指针.为什么允许在新的贴图中使用指向const数据的指针? (6认同)
  • @ClaasBontus它是一个指向``X``的指针,而不是``const int``.如果你想要在具有``const``成员的结构上工作,你需要允许这个,这通常是你想要的.甚至``const int``也是有道理的.考虑为大量小型,同等大小的对象定制分配器,将它们全部放在为此目的分配的内存块中. (6认同)
  • @ user2899162嗯,没有.这是为了"惩罚"名称或指针,这样它们可以合法地用于访问不同于其原始指示对象的对象实例/生命周期.这不是打字,它仍然像UB一样. (6认同)
  • @ Random832因为你并不总是需要演员表.洗钱的目的不是进行类型转换.这是使类型转换(和其他各种形式的聪明,不涉及演员表)合法.如果您只是自己进行了类型转换,那么您将通过严格别名规则获得UB. (5认同)
  • @supercat:"*你能想出任何理由......*"你向错误的人抱怨.如果您不喜欢C++在这方面的定义,请与标准委员会联系.我只是在解释标准是如何起作用的. (5认同)
  • @Mehrdad:"*例如,如果有这样的数据成员,但它从未被访问过怎么办?*"不相关.当你需要清洗记忆时,[basic.life]/8会拼出来.特定编译器可能在特定结构上不需要它并不重要.*标准*需要它. (4认同)
  • @NicolBolas:你能想到为什么那些被认为对"优化"感兴趣的人会无法提供一种方法来让程序员安全地使用可提供2倍或更好速度提升的技术(在某些情况下可以允许订单)处理某些API时速度提升的程度).我理解有必要让编译器在没有理由相信它们不安全的情况下进行优化假设*,但要求程序员编写效率低下的代码似乎不是一个良好性能的配方. (4认同)
  • 为了证实我的理解是正确的,如果你改写了这最后一节为'INT*totallyNewPtr =新的(数据)INT;`,就没有必要洗钱任何东西,因为`totallyNewPtr`是一个新鲜的指针,没有按"对象是否需要通过任何过期的对象访问? (4认同)
  • 您的例子中是否有需要联合的原因?普通结构不一样吗? (3认同)
  • @templatetypedef 只要您不使用其他指针,就可以。 (3认同)
  • 但是你_have_做了一个reinterpret_cast。为什么它不像`std::launder&lt;int *&gt;(&amp;data)`(实际形式参数是空指针)? (2认同)
  • @Mehrdad:那里我们不需要else子句.您希望解决的情况按原样解决.该标准非常清楚*如果访问过`const`限定的成员,它*无关紧要.成员存在的事实足以要求对整个包含类型进行"清洗". (2认同)
  • @supercat:C++不允许你进行"就地类型惩罚".不是一般意义上的.您可以获得的最接近的是联合的新措辞,但这不允许您通过不同类型的对象访问该值.C++仅支持按副本进行打字. (2认同)
  • @cmaster 我已将其保存在[此处](http://nosubstance.me/post/dereferencing-null-pointers/)。该问题在不到 15 分钟内就被关闭,并在不到 24 小时内被删除。梅塔也不在乎。而且,我实际上是预料到并故意这样做的。作为保障,我已经准备好了问题和答案,以便在它关闭之前同时发布。这件事发生了,我知道人们已经在尝试回答,但不能再回答了。此后我已停止发布问题/答案。我只是想看看它有多不友好。甚至有人问我是否懂得拉丁语来使用某个特定术语。 (2认同)
  • 是否有已知情况在已知编译器上实际需要它?上面的示例即使使用 -O3 也不需要使用 --std=c++11 或 --std=c++17 在 clang 中进行清洗。 (2认同)
  • @einpoklum:它可以这样做,因为标准*说它可以*。语言和库之间没有区别;它们都是一回事,并且它们都对 C++ 拥有法律上的权威。因此,如果标准说 `launder` 提供了一个指向该内存中活动对象的指针,那么它提供了一个指向该内存中活动对象的指针。 (2认同)
  • @einpoklum:这是“实施”的含义。与代码生成有关的“洗钱”行为是您在某些情况下获得明确定义的行为。该行为是由*编译器*生成的;它看到您进行了内存洗钱,因此它无法再假设它本来可以假设的事情。也就是说,当优化器正在执行并将您的 `uxn` 使用转换为文字 `1` 时,它会遇到 `launder` 并发出“哎呀,看不透”然后停止。 (2认同)
  • @Miral:但是放置“new”*不会*修改对象;它结束旧对象并在其位置创建一个新对象。C++ 中的变量名称 *objects*; 如果你在一个对象上放置“new”,那么该变量就不再命名它。C++ 在某些情况下允许旧对象的名称引用新对象。但在这些情况之外,该名称指的是旧对象。您想要的是一个名称不命名对象的系统,因此,任何“const”声明的名称都不能被假定为引用相同的“const”对象。那么 `const` 实际上会做什么呢? (2认同)
  • 不管它是否是同一个对象,编译器都可以看到新的放置位置,并且可以知道标签 `ux` 所指的任何内容都已被修改或替换。因此,事后提及此的任何内容都不应假定它没有被更改。同样,不需要洗钱。要求程序员明确地把这种东西放进去是愚蠢的。 (2认同)
  • @Miral:除非它*不能*看到它,因为它位于预编译库的后面。或者从 DLL 加载的东西。或者出于任何其他原因,编译器会选择*不* 通过函数调用内联。或者...如果放置-`new`在*条件*语句内,那么编译器*不可能知道*在编译时发生了哪个分支。要求编译器知道它不可能知道的事情是愚蠢的。 (2认同)
  • @Miral:无法优化什么?如果您在创建“u”和访问它之间调用不透明函数,即使您传递对“u”的引用作为参数,编译器当然可以优化“uxn”。为什么?因为“const”对象*不能*被更改,因此任何这样做的代码都会引发 UB。因此返回原始值是有效的“未定义”行为。您所要求的只是在某些不太具体的情况下使其不被定义。何必呢?为什么不尊重“const”对象的名称始终引用原始“const”对象呢? (2认同)
  • 来得很晚,但是为什么我们需要工会来激励榜样?在单个对象中新建一个简单的放置位置不会完全一样吗? (2认同)
  • 根据最新的标准草案(参见答案中的链接),该答案链接到的 [basic.life] 段落现在使得在此答案的示例中无需进行清洗,因为 union 和 const int 都可以被新的对应项轻松替换。有关更合适的示例,请参阅该草案的 std::launder 部分,链接在同一段落的底部。 (2认同)
  • @NicolBolas:值得一提——第一个激励示例不再相关[自 C++20 起](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/p1971r0.html )。 (2认同)

F.v*_*.S. 18

我认为有两个目的std::launder

  1. 一个障碍不断折叠/传播,包括去虚拟化。
  2. 基于细粒度对象结构的别名分析的障碍。

过度激进的持续折叠/传播的屏障(废弃)

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

在 C++17/ P0137R1中,std::launder引入了作为禁用上述(错误)优化 ( CWG 1776 ) 的功能,这是std::optional. 正如P0532R0std::vector中所讨论的,和 的可移植实现std::deque也可能需要std::launder,即使它们是 C++98 组件。

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

去虚拟化的障碍

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

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

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

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

P0137R1还通过引入指针互换性改变了 C++ 对象模型。IIUC 这样的改变使得N4303中提出的一些“基于对象结构的别名分析”成为可能。

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

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

与基于类型的别名分析/严格别名的关系

IIUCstd::launder和基于类型的别名分析/严格别名无关。std::launder需要正确类型的活动对象位于所提供的地址。

然而,它们似乎在 Clang 中意外地关联起来(LLVM-D47607)。


ein*_*ica 6

std::launder是一个误称。此函数执行清洗相反的操作:它污染指向内存,以消除编译器可能对指向值的任何期望。它排除了基于此类期望的任何编译器优化。

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

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

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

...这使我对这std::launder意味着什么有了这种看法。

  • 我不知道,似乎对我来说完全是洗钱:它删除了指针的来源,以便它是干净的,并且需要(重新)读取。我不知道“弄脏”在这种情况下意味着什么。 (12认同)
  • 回复:“_那个地方现在被弄脏了,不要做出这样的假设_”-或者,“那个地方_被弄脏了,请`std::launder`它” (4认同)
  • *“此功能执行与清洗相反的操作:它弄脏了指向的内存,”*不,它不会。完全不影响记忆。编译器可以继续保留有关指向该内存的现有指针的过时假设。但它不能将这些假设转移到从 `std::launder` 返回的新指针,即使它的数值是相同的。新指针已被**清洗**。任何旧指针都没有。只有通过新指针的访问才会受到影响。调用“launder”只会产生一个新的“laundered”指针,并且只会影响通过该指针的访问。 (4认同)
  • 我同意如果“std::launder”是指洗钱,那么它的命名完全相反,但我认为你不应该说它污染了记忆。无论是否“洗”,脏钱都是脏的,但洗钱会让人们错误地认为它是干净的。无论是否经过“std::launder”,脏内存都是脏的,但清洗会使编译器“停止”错误地认为它是干净的。 (3认同)