使用 std::launder 从指向非活动工会成员的指针获取指向活动工会成员的指针?

Using std::launder to get a pointer to an active union member from a pointer to an inactive union member?

本文关键字:成员 指针 获取 取指 活动 std 使用 launder 非活动      更新时间:2023-10-16

考虑这个并集:

union A{
int a;
struct{
int b;
} c;
};

ca不是布局兼容的类型,因此无法通过a读取b的值:

A x;
x.c.b=10;
x.a+x.a; //undefined behaviour (UB)

对于试验 1 和试验 2,请参阅此问题

试用 3

现在让我们将std::launder用于它似乎不打算做的事情:

A x;
x.a=10;
auto p = &x.a;                 //(1)
x.c.b=12;                      //(2)
p = std::launder(p);           //(2')
*p+*p;                         //(3)  UB?

std::launder能改变什么吗?根据[ptr.launder]:

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

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

返回:指向XT *类型的值。

备注: 只要核心常量表达式的参数值可用于核心常量表达式,就可以在核心常量表达式中使用此函数的调用。可以通过指向对象 Y 的指针值(如果对象 Y 在 Y 占用的存储内)、指针可与 Y 相互转换的对象或紧紧封闭的数组对象(如果 Y 是数组元素)来访问存储字节。如果 T 是函数类型或 cv void,则程序格式不正确。

粗体句子强调了一个困扰我的东西。如果p是无效的指针值,如何访问任何存储字节?另一方面,使用这样的读数std::launder是无法使用的。

否则,p在 (2) 处的值是否可以是一个指针值,表示存储区域,如 [basic.life] 中的"注释"中所述

如果不满足这些条件,则可以通过调用std​::​launder([support.dynamic]) 从表示其存储地址的指针获取指向新对象的指针。

在注释中明确允许。

basic.life包含以下规则,使std::launder变得不必要:

如果在对象的生存期结束后,在重新使用或释放对象所占用的存储

之前,在原始对象占用的存储位置创建一个新对象、指向原始对象的指针、引用原始对象的引用或原始对象的名称,并且, 一旦新对象的生存期开始,可用于操作新对象,如果:

  • 新对象的存储完全覆盖原始对象占用的存储位置,并且
  • 新对象与原始对象的类型相同(忽略顶级 CV 限定符),并且
  • 原始对象的类型不是常量限定的
  • ,如果是类类型,则不包含任何类型为常量限定或引用类型的非静态数据成员,并且
  • 原始对象和新对象都不是可能重叠的子对象。

[ 注意:如果不满足这些条件,则可以通过调用std::launder从表示其存储地址的指针获取指向新对象的指针。 — 尾注 ]

这种"在原始对象占用的存储位置创建新对象"的情况在这里显然适用,因为:

已创建一个对象...当隐式更改工会的活跃成员时...

满足所有项目符号条件,因为"潜在重叠的子对象"是指基类子对象,而联合成员不是。 (在您链接到的版本中,该项目符号直接提到了基类子对象。

然而,即使这种解释要改变,反对工会,说明也特别提到std::launder绕过了这一限制。

请注意,旧版本的标准从此规则中排除了子对象...但该说明清楚地表明,std::launder也会绕过这个问题。