std::launder能否用于将对象指针转换为其所在数组的指针?

15

当前的草案标准(以及预计的C++17)在[basic.compound/4]中提到:

[注意:数组对象及其第一个元素虽然具有相同的地址,但它们不能互相转换为指针。— 结束注意事项 —]

因此,一个对象的指针不能通过reinterpret_cast转换为其所在的数组指针。

现在,有了std::launder[ptr.launder/1]

template<class T> [[nodiscard]] constexpr T* launder(T* p) noexcept;

要求:p表示内存中一个字节的地址A。一个类型与T相似且在其生命周期内的对象X位于地址A处。通过结果可以访问到的所有存储字节也可以通过p访问到(见下文)。

可访问的定义在[ptr.launder/3]中:

备注:只要其参数的值可以在核心常量表达式中使用,就可以在核心常量表达式中使用此函数的调用。如果通过指向对象Y的指针值可以访问存储器中的一个字节,那么该字节就是通过指向Y的指针值、与Y可互相转换的对象或者如果Y是数组元素,则是立即封闭的数组对象而可访问的。如果T是函数类型或cv void,则程序是非法的。

现在,乍一看,似乎std::launder可以用来进行上述转换,因为我强调了其中的一部分。

但是。如果p指向一个数组对象,根据这个定义,数组的字节是可访问的(即使p与数组指针不可互相转换),就像launder的结果一样。因此,这个定义似乎对这个问题没有任何说明。

那么,std::launder可以用来将对象指针转换为其所属数组指针吗?

这里的“可达性”是一个术语。 “备注:”段的后半部分仅是“要求:”段中术语“可达性”的解释/定义。 - xskxzr
5
对于数组,“可达性”指的是简单的指针算术,例如 p + 1。您可以从任何一个元素到达任何其他元素。然而,指针互换性是指转换为指向整个数组的指针,例如 int (*) [10],这是完全不同的故事。您不需要这种可互换性即可访问其他元素。 - AnT stands with Russia
@AnT:感谢您的澄清! - geza
2个回答

11
这取决于封闭的数组对象是否是一个完整的对象,如果不是,则取决于您是否可以通过指向该封闭的数组对象的指针有效地访问更多字节(例如,因为它本身是一个数组元素,或者可与较大的对象进行指针互换,或者可与作为数组元素的对象进行指针互换)。"可到达"要求意味着您不能使用 launder 来获得一个指针,该指针将允许您访问比源指针值允许的更多字节,否则将导致未定义行为。这确保了某些未知代码可能调用 launder 的可能性不会影响编译器的逃逸分析。
我想一些示例可能会有所帮助。下面的每个示例都会将指向10个int数组的第一个元素的int*重新解释为int(*)[10]。由于它们不能进行指针互换,因此reinterpret_cast不会改变指针值,并且您将得到一个int(*)[10],其值为“指向数组的第一个元素的指针”。然后,每个示例都尝试通过在强制转换指针上调用std::launder来获取对整个数组的指针。
int x[10];
auto p = std::launder(reinterpret_cast<int(*)[10]>(&x[0])); 

这是没问题的;你可以通过源指针访问x的所有元素,并且launder的结果不允许你访问其他任何东西。

int x2[2][10];
auto p2 = std::launder(reinterpret_cast<int(*)[10]>(&x2[0][0])); 

这是未定义的。您只能通过源指针访问x2[0]元素,但结果(指向x2[0]的指针)将允许您访问x2[1],您无法通过源进行访问。

struct X { int a[10]; } x3, x4[2]; // assume no padding
auto p3 = std::launder(reinterpret_cast<int(*)[10]>(&x3.a[0])); // OK

这是可以的。同样地,您不能通过指向 x3.a 的指针访问任何一个您已经无法访问的字节。

auto p4 = std::launder(reinterpret_cast<int(*)[10]>(&x4[0].a[0])); 

这应该是未定义的。您本来可以从结果中访问x4[1],因为x4[0].ax4[0]可以相互转换指针,因此前者的指针可以被reinterpret_cast转换为后者的指针,然后可以用于指针算术运算。请参见https://wg21.link/LWG2859

struct Y { int a[10]; double y; } x5;
auto p3 = std::launder(reinterpret_cast<int(*)[10]>(&x5.a[0])); 

这仍然是未定义的,因为您可以通过reinterpret_cast将结果指针(到Y*)转换来从x5.y访问,但源指针不能用于访问它。


1
所以根据这种推理,表达式的_source_以某种方式影响了std::launder的语义?无论优化器如何实现,这听起来都像是一个疯狂的心理模型。这让我想起了整个指针来源的事情。 - Passer By
也许 std::launder 正是为那些“精神模型”本身就很疯狂的情况而设计的。;-) - André
std::launder 的规范并没有涉及到 reinterpret_cast 的参数,或者说根本没有提到 reinterpret_cast。它只阐述了关于 std::launder 的参数和结果的内容。也就是说,在你考虑的情况下,它只涉及到 reinterpret_cast结果 - n. m.
是的,但它并不是“指向整个数组”的指针,尽管它具有相同的值。你需要使用std::launder来实现这一点。这就是整个重点。 - n. m.
没错,这些例子的目的是为了说明什么情况下可以和不能进行洗钱。 - T.C.
显示剩余5条评论

2

备注:任何非精神分裂的编译器都可能欣然接受这个,就像它会接受C风格的转换或重新解释的转换一样,所以试试看并不是一个选项。

但是,在我看来,对于你的问题,答案是否定的。强调的“如果Y是数组元素,则立即包含的数组对象”位于“备注”段落中,而不是“要求”段落中。这意味着只要尊重要求部分,备注部分也适用。由于数组及其元素类型不是相似类型,因此未满足要求,不能使用std::launder

接下来是更一般(哲学?)的解释。在K&R C时代(70年代),C旨在能够替代汇编语言。出于这个原因,规则是:只要源代码可以被转换,编译器必须遵守程序员提供的规则。因此没有严格的别名规则,指针不再是具有附加算术规则的地址。这在C99和C++03中发生了很大的变化(不谈论C++11 +)。现在,程序员应该将C++作为高级语言使用。这意味着指针只是允许访问给定类型的另一个对象的对象,数组及其元素类型是完全不同的类型。内存地址现在只是实现细节。因此,尝试将指向数组的指针转换为指向其第一个元素的指针违反了语言的哲学,并且可能会在编译器的后续版本中咬住程序员。当然,现实生活中的编译器仍然接受它以保持兼容性,但我们不应该在现代程序中尝试使用它。


1
一个数组位于其第一个元素的相同地址,而且数组类型肯定与自身相似,因此需要使用 requires 部分。 - n. m.
@n.m.:只是为了澄清一下:你的意思是答案是“是”吗? - geza
是的,我也这么认为。免责声明:这是在我的一个问题中的回答(已被管理员删除)中提到的。 - n. m.
@n.m.:我刚在 Meta 上问了一个关于这个问题的问题(链接:https://meta.stackoverflow.com/q/371664/3545273)。 - Serge Ballesta
@LanguageLawyer 我试图编辑您的帖子以使其成为答案,但对于第三方编辑来说,它改变得太多了。如果您同意我的编辑,请添加一个新的编辑以明确您接受并在评论中ping我,以便我可以要求取消删除该帖子。 - Serge Ballesta
显示剩余2条评论

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