为什么这个std::launder示例是未定义行为?

8

这是我不理解的示例部分:

struct Y
{
    int z;
};
int main()
{
    alignas(Y) std::byte s[sizeof(Y)];
    Y *q = new (&s) Y{2};
    const int f = reinterpret_cast<Y *>(&s)->z; // Class member access is undefined behavior:
                                                // reinterpret_cast<Y*>(&s) has value "pointer to s"
                                                // and does not point to a Y object
}

整个示例在https://en.cppreference.com/w/cpp/utility/launder

我不明白为什么使用reinterpret_cast<Y *>(&s)->z是未定义行为。我们已经在所谓的“指向s的指针”处构造了一个Y对象,并将其重新解释为Y*,那么为什么它仍然说“它没有指向Y对象”?


据我所见,无论类型如何,s、q和reinterpret_cast<Y *>(&s)的值都相同。 - xxhxx
就我所见,无论类型如何,s、q和reinterpret_cast<Y *>(&s)的值都相同。但这与确定某些内容是否UB无关。如果标准规定它是UB,编译器可以假设您不会这样做,并且可以生成任何代码。实际上,任何行为都是可以接受的。 - Jesper Juhl
1个回答

11

&s是指向std::byte[sizeof(Y)]的指针,而不是Y。这意味着将&s用作Y*是未定义行为,因为它违反了别名规则。即使有一个Y存在,reinterpret_cast规则(特别是类型别名部分)也不允许您访问该对象,因为它无法知道实际上是否存在对象。

std::launder被引入以明确允许您将返回的指针视为对对象的有效指针,而不是指向对象所在存储的指针。实质上,它是类型别名规则的显式覆盖。使用它告诉编译器您知道该指针确实指向有效对象,如果您撒谎了,那么就会回到未定义的行为领域。


如果有人想知道,别名规则主要是为了允许有效地“重新排序”不影响同一对象的语句(尽管实际情况并非如此,因为你描述的是一个程序,而不是编程计算机!编译器会为您编写计算机程序...) - Asteroids With Wings
顺便提一下,UB可以通过正确使用“s”轻松修复:只需声明“alignas(Y) unsigned char s[sizeof(Y)];”并丢弃多余的“&”运算符即可。然后,“s”将被衰减为“unsigned char*”,而该类型不受严格别名规则的约束。因此,“reinterpret_cast<Y*>(s)->z”是明确定义的。 - cmaster - reinstate monica
5
不行,那仍然是未定义行为。将对象转换为char/byte并读取其字节是合法的,但是如果不使用从定位new返回的指针或者使用std::launder,把存储池当作对象来处理是非法的。 - NathanOliver
请自行表述 ;) 这正是为什么需要 std::launder 的原因。 - Asteroids With Wings
然而,C++20会使这更为复杂,因为可以将“隐式类型”的存储池用作如果对象存在而不需要放置new或launder。 - NathanOliver
虽然它正式上属于 UB,但我认为 std::launder 的引入是为了找到一种符合规范的方式来做几十年来一直在实践的事情:将一个“无类型”指针转换为我们知道存在或可能存在的“对象”的类型,出于某种原因。一个主要的例子是在已知地址进行硬件访问。另一个例子是使用 malloc 返回的内存,参见 https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2020/p0593r6.html。没有编译器会为 int *pi = (int *)malloc(sizeof(int)); *pi = 1; 生成无效的代码,即使那里并没有 int。 - undefined

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