reinterpret_cast可以将无效的指针值转换为有效的指针值

Could reinterpret_cast turn an invalid pointer value into a valid one?

本文关键字:指针 有效 转换 无效 cast reinterpret      更新时间: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

对于下面的情况,我认为自 C++17 以来,我也得到了一个未定义的行为:

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

让我们考虑 [basic.type]/3:

指针类型的每个值都是以下值之一:

指向对象或函数的指针
  • (指针被称为指向对象或函数),或
  • 超过对象末尾的指针([expr.add]),或
  • 该类型的空指针值([conv.ptr]),或
  • 无效的指针值

我们将这 4 个指针值类别称为指针值类型

指针的值可能会从上述类型过渡到另一种类型,但标准对此并不明确。如果我错了,请免费填写以纠正我。所以我认为在 (1) 处,p的值是指向值的指针。然后在 (2) 中a生命结束,p的值变为无效的指针值。所以在(3)中,我得到UB,因为我试图访问一个对象(a)的值。

试用 2

现在考虑这个奇怪的代码:

A x;
x.a=10;
auto p = &x.a;                 //(1)
x.c.b=12;                      //(2)
p = reinterpret_cast<int*>(p); //(2')
*p+*p;                         //(3) UB?

reinterpret_cast<int*>(p)是否可以将指针值类型从invalid pointer value更改为pointer to值。

reinterpret_cast<int*>(p)被定义为等价于static_cast<int*>(static_cast<void*>(p)),那么让我们考虑如何定义从void*int*static_cast,[expr.static.cast]/13:

类型为"指向cv1 void的指针"的 prvalue 可以转换为"指向cv2 T的指针"类型的 prvalue,其中T是对象类型,cv2cv1的 cv 资格相同,或更高 cv 资格。如果原始指针值表示内存中某个字节的地址A,而A不满足T的对齐要求,则未指定生成的指针值。否则,如果原始指针值指向对象a,并且存在类型为T(忽略 cv-限定)的对象 b,该对象可与a进行指针互转换,则结果是指向b的指针。否则,指针值在转换时保持不变。

所以在我们的例子中,原始指针指向对象a.所以我想reinterpret_cast不会有帮助,因为a不在它的生命周期内。我的阅读是严格的吗?这段代码可以很好地定义吗?

然后在 (2) 中,生命结束,p 的值变为无效的指针值。

不對。指针仅在指向已结束其存储持续时间的内存时失效。

在这种情况下,指针将变为指向对象生存期之外的指针。它指向的对象消失了,但指针并没有像规范所表示的那样"无效"。[basic.life] 花了相当多的时间来解释你可以做什么和不能做什么来指向其生命周期之外的对象。

reinterpret_cast不能将指向其生存期之外的对象的指针转换为指向其生存期内的其他对象的指针。

标准中对象的概念相当抽象,与直觉有些不同。一个对象可能在其生命周期内,也可能不在生命周期内,而不在其生命周期内的对象可以具有相同的地址,这就是联合工作的原因:活动成员的定义是"在其生命周期内的成员"。

指向不在其生存期内的对象的指针仍然是指向对象的指针。reinterpret_cast只在指针类型之间进行强制转换,而不强制转换其有效性。强制转换为非指针可互转换类型时获得的 UB 是由于严格别名规则,而不是由于指针的有效性。

在您的所有试验中,包括您的后续问题,您正在以不允许的方式使用不在其生命周期内的对象,即访问它,因此是 UB。

迄今为止,C和C++标准的每个版本对于工会成员的地址可以做什么都是模棱两可或矛盾的。 C 标准的作者不想要求编译器悲观地考虑函数可能被以下构造调用的可能性:

someFunction(&myUnion.member1, &myUnion.member2);

在函数会导致myUnion的一个成员的值在通过另一个成员进行的访问之间更改的情况下。 虽然如果代码不能做这样的事情,那么获取工会成员地址的能力将毫无用处:

someFunction1(&myUnion.member1);
someFunction2(&myUnion.member2);
someFunction3(&myUnion.member1);

该标准的作者期望用于各种目的的质量实现将处理未定义行为的构造,当这样做时,"以记录的环境特征"最能满足这些目的,因此认为支持此类结构是一个实现质量问题将比试图制定必须支持哪些模式的精确规则更简单。 在不知道调用上下文的情况下为第二个示例中的被调用函数生成代码的编译器将无法交错访问这两个函数执行的访问,而在处理上述代码时内联扩展它们的质量编译器将毫不费力地注意到每个指针何时派生自myUnion

C89 标准的作者认为没有必要为指向工会成员的指针的行为定义精确的规则,因为他们认为编译器编写者对产生高质量实现的愿望会驱使他们明智地处理适当的情况,即使没有这样的规则。 不幸的是,一些编译器编写者懒得处理像上面第二个例子这样的情况,而不是认识到高质量的编译器没有任何理由不能处理这种情况,后来的 C 和 C++ 标准的作者已经向后弯腰,提出了奇怪的扭曲、模棱两可和矛盾的规则来证明这种编译器行为的合理性。

因此,只有在生成的指针将用于访问存储的单个字节的情况下,才应将 address-of 运算符视为对联合成员有意义,无论是直接使用字符类型,还是传递给以这种方式定义的函数(如memcpy)。 除非或直到对标准进行了重大修改,或者附录描述了实现可以提供超出标准要求的可选保证的方法,否则最好假装工会成员是 - 就像位字段 - 没有地址的lvalues。