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

37

我刚刚阅读了std::launder的用途是什么?

老实说,我现在有点摸不着头脑。

让我们从@NicolBolas的回答中第二个示例开始:

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

[basic.life]/8告诉我们,如果您在旧对象的存储中分配一个新对象,则无法通过指向旧对象的指针访问新对象。std::launder可以帮助我们避开这个问题。

那么,为什么不直接更改语言标准,使通过 reinterpret_cast<int *>(&data)访问data是有效/适当的呢?在现实生活中,洗钱是一种隐藏事实的方式以躲避法律。但我们没有任何需要隐藏的东西-我们在这里做的是完全合法的事情。那么,当编译器注意到我们以这种方式访问data时,为什么不能将其行为更改为其std::launder()行为?

接下来是第一个示例:

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

因为X是微不足道的,所以我们在创建一个新对象来替换旧对象之前无需销毁旧对象,因此这是完全合法的代码。新对象将具有n成员2。

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

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

那么,为什么不让编译器在我们通过指针访问常量字段时,不允许做出这种假设呢?

为什么要有这种伪函数来打破正式语言语义,而不是根据代码是否像这些示例一样执行某些操作来设置语义?


1
我猜launder在一个非常小的范围内禁用了优化。 - dyp
4
那为什么不让编译器在我们编写这种代码时不做出假设呢?” 你的意思是,除了编译器大多数时间必须生成次优代码这一明显事实之外?大多数人并不会在存储中重新创建对象,因此假设 u.x.n 不会改变是完全合理的。 - Nicol Bolas
1
@dyp: 猜猜看?它必须假设已经存在 already - einpoklum
1
@einpoklum: "它必须假设已经这样了。" 你把编译后的代码和标准所要求发生的事情混淆了。标准中没有明确要求编译器假设任何类似的事情。 - Nicol Bolas
1
@NicolBolas:你是在说这个例子只是一个被忽略的优化吗?我认为对函数值使用“const”限定符并不是向编译器提供保证,而是一种安全预防措施,让编译器防止你作为作者去更改它。 - einpoklum
显示剩余10条评论
2个回答

22

根据代码是否像这些示例一样执行操作而定。

由于编译器不能总是知道何时“以这种方式”访问数据。

目前情况下,编译器可以假设以下代码:

struct foo{ int const x; };

void some_func(foo*);

int bar() {
    foo f { 123 };
    some_func(&f);
    return f.x;
}

bar 函数将总是返回 123。编译器可能会生成实际访问对象的代码,但对象模型并不要求如此。 f.x 是一个 const 对象(而不是指向 const 的引用/指针),因此它不能被改变。而且 f 必须始终指定相同的对象(事实上,这些是需要更改的标准的部分)。因此,f.x 的值不能通过任何非 UB 方法更改。

为什么在正式语言语义中加入这个伪函数是合理的?

这个问题曾经被讨论过。该论文提到了这些问题存在的时间之久(即:自 C++03 以来),并且通常利用这个对象模型实现了优化。

该提议因为无法真正解决问题而被拒绝。根据此行程报告

然而,在讨论中,有人指出,所提出的替代方案无法处理所有受影响的情况(特别是在虚函数表指针发挥作用的情况下),并且它没有得到共识。

该报告并未详细说明该问题,相关讨论也不公开。但提案本身指出,它将不允许对第二个虚函数调用进行虚拟化,因为第一次调用可能建立了一个新对象。因此,即使是 P0532 也不会使 launder 成为不必要,只是变得不那么必要。


2
关于您的第二条评论,编译器缺少优化并不足以怀疑标准。错过优化的其他原因包括:没有人投入工作将此案例添加到优化器中;用户群体期望非标准行为。类似情况没有结构体会被优化;带有const成员的结构体很少见。 - M.M
5
在[xcl.type.cv/4]中,通过使用const_cast在x的生命周期内修改f.x是未定义行为。在some_func内结束x和/或f的生命周期本身是允许的,但这会导致在[基本生命期/(8.3)]中的bar中有序后访问成为未定义行为。 - aschepler
2
@einpoklum: "我的意思是,const_cast后跟着对f.x的修改。" [dcl.type.cv]/4没有针对const_cast使用的异常。一个对象是否为常量并不等同于一个对象的引用是否为常量。声明为常量的对象是一个const对象,根据[dcl.type.cv]/4,任何导致其值被修改的操作都会自动成为未定义行为。 - Nicol Bolas
2
说得对。但是考虑到这种情况,我会说std::launder的命名相反。std::soil会更加合适。 - einpoklum
4
我认为"常量子对象"的例子已经不再相关:https://github.com/cplusplus/draft/commit/fd8ff6441f93024bd0ee6e03a03c08be8e1b5ce0 - dyp
显示剩余10条评论

-2
我采取相反的方法。我不喜欢C++这样随意地玩弄内存,或者至少允许你这样做。当尝试创建一个新对象并且地址包含常量值或对象但具有const成员时,编译器应该抛出错误。如果程序员说是const,那就是const。抱歉。

1
这似乎更像是对我的问题的评论,而不是答案。 - einpoklum

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接