Hol*_*Cat 9 c++ language-lawyer
下面是一个例子:
#include <cstddef>
#include <iostream>
struct A
{
char padding[7];
int x;
};
constexpr int offset = offsetof(A, x);
int main()
{
A a;
a.x = 42;
char *ptr = (char *)&a;
std::cout << *(int *)(ptr + offset) << '\n'; // Well-defined or not?
}
Run Code Online (Sandbox Code Playgroud)
我一直认为它是明确定义的(否则有什么意义offsetof),但不确定。
最近有人告诉我它实际上是UB,所以我想一劳永逸地弄清楚。
上面的例子是否会导致UB?如果将类修改为非标准布局,是否会影响结果?
如果是 UB,是否有任何解决方法(例如申请std::launder)?
整个主题似乎都没有实际意义且没有具体说明。
以下是我能找到的一些信息:
是否添加到“char *”指针 UB,当它实际上并不指向 char 数组时?- 2011 年,CWG 确认允许我们通过unsigned char指针检查标准布局对象的表示。
char可以使用指针代替,常识说它可以。不清楚是否std::launder需要将来自 C++17 的凝视应用于转换的结果(unsigned char *)。鉴于这将是一个突破性的变化,这可能是不必要的,至少在实践中是这样。
不清楚为什么 C++17 改为offsetof有条件地支持非标准布局类型(以前是 UB)。这似乎暗示,如果实现支持这一点,那么它还允许您通过unsigned char *.
在标准布局对象(例如,使用 offsetof)中进行指针运算时,我们是否需要使用 std::launder ?- 与此类似的问题。没有给出明确的答案。
这里我将参考C++20(草案)措辞,因为C++17和C++20之间修复了一个相关的编辑问题,并且可以参考HTML版本的C++20中的具体句子草案,但与 C++17 相比没有什么新内容。
\n\n首先,指针值的定义[basic.compound]/3:
\n\n\n\n\n指针类型的每个值都是以下之一:
\n
\n \xe2\x80\x94指向对象或函数的指针(该指针被称为指向对象或函数),或
\n \xe2\x80\x94 a超过对象末尾的指针
([expr.add]),或\n \xe2\x80\x94该类型的空指针值
,或\n \xe2\x80\x94无效的指针值。
现在,让我们看看表达式中发生了什么(char *)&a。
让我不证明这a是一个表示类型 的对象的左值A,我会说 \xc2\xabthe object a\xc2\xbb 来引用这个对象。
子表达式的含义&a包含在[expr.unary.op]/(3.2)中:
\n\n\n如果操作数是 类型的左值
\nT,则结果表达式是 \xe2\x80\x9c 类型的纯右值,指向T\xe2\x80\x9d 的指针,其结果是指向指定对象的指针
因此,&a是类型的纯右值A*,其值为 \xc2\xab指向(对象)a\xc2\xbb 的指针。
现在,强制转换(char *)&a相当于reinterpret_cast<char*>(&a),其定义为static_cast<char*>(static_cast<void*>(&a))( [expr.reinterpret.cast]/7 )。
转换为void*不会更改指针值 ( [conv.ptr]/2 ):
\n\n\n指向cv
\nT\xe2\x80 \x9d 的类型为 \xe2\x80\x9c 的纯右值(其中T是对象类型)可以转换为类型为 \xe2\x80\x9c 的指向cvvoid\xe2\x80\x9d 的指针的纯右值。此转换不会改变指针值 ([basic.compound])。
即它仍然是指向(对象)a\xc2\xbb 的\xc2\xab 指针。
[expr.static.cast]/13覆盖外部static_cast<char*>(...):
\n\n\n指向cv1
\nvoid\xe2\x80\x9d 的 \xe2\x80\x9c 指针类型的纯右值可以转换为指向cv2T\xe2\x80\x9d的 \xe2\x80\x9c 指针类型的纯右值,其中T是对象类型,cv2是与 cv1 相同的 cv 限定,或比cv1更大的 cv 限定。\n 如果原始指针值表示内存中字节的地址 A,并且 A 不满足 的对齐要求T,则结果指针值未指定。\ n 否则,如果原始指针值指向对象a,并且存在可与a指针相互转换的类型的对象b (忽略 cv 限定) ,则结果是指向b的指针。\n 否则,指针转换后值不变。T
不存在可char与对象a( [basic.compound]/4 ) 进行指针互换的类型的对象:
\n\n\n两个对象a和b是指针可相互转换的,如果:
\n
\n \xe2\x80\x94 它们是同一个对象,或者
\n \xe2\x80\x94 一个是联合对象,另一个是以下对象的非静态数据成员该对象 ([class.union]) 或
\n \xe2\x80\x94 一个是标准布局类对象,另一个是该对象的第一个非静态数据成员,或者,如果该对象没有非静态数据成员, -静态数据成员,该对象的任何基类子对象([class.mem]),或
\n \xe2\x80\x94 存在一个对象c,使得a和c是指针可相互转换的,并且c和b是指针-可相互转换。
这意味着static_cast<char*>(...)不会更改指针值,并且与其操作数中的值相同,即:\xc2\xab指向 a\xc2\xbb 的指针。
因此,(char *)&a是类型的纯右值char*,其值为 \xc2\xab指向 a\xc2\xbb 的指针。该值存储到char* ptr变量中。然后,当您尝试使用这样的值(即 )进行指针算术时ptr + offset,您将进入[expr.add]/6:
\n\n\n对于加法或减法,如果表达式
\nP或 的Q类型为 \xe2\x80\x9c 指向cvT\xe2\x80\x9d 的指针,其中T和 数组元素类型不相似,则行为未定义。
出于指针算术的目的,该对象a被视为数组的元素A[1]([basic.compound]/3),因此数组元素类型为A,指针表达式的类型P为 \xc2\xabpointer to char\xc2\ xbbchar和A不是相似的类型(请参阅[conv.qual]/2),因此行为未定义。
这个问题和另一个关于 的问题launder,在我看来都可以归结为对 C++17 [expr.static.cast]/13 最后一句的解释,其中涵盖了发生的情况static_cast<T *>指向不相关的指针的操作数时发生的情况正确对齐的类型:
\n\n\n指向cv1
\n\nvoid\xe2\x80 \x9d 的 \xe2\x80\x9c 指针类型的纯右值可以转换为指向cv2的 \xe2\x80\x9c 指针类型的纯右值T\xe2\x80\x9d的 \xe2\x80\x9c 类型的指针的纯右值,[...]
\n\n否则,指针值不会因转换而改变。
\n
一些海报似乎认为这意味着强制转换的结果不能指向类型的对象T,因此reinterpret_cast指针或引用只能用于指针可相互转换的类型。
但我不认为这是合理的,并且(这是一个反证法证)该立场还意味着:
\n\nunsigned char *或任何字符类型都不能用于访问该字节)。offsetof没有用(因此 C++17 对它的更改是多余的)对我来说,这句话的意思是强制转换的结果指向内存中与操作数相同的字节,这似乎是更明智的解释。(与指向某个其他字节相反,这可能发生在本句未涵盖的某些指针转换中)。说“值不变”并不意味着“类型不变”,例如,我们将从 到 的转换描述int为long保留值。
另外,我想这可能对某些人来说是有争议的,但我认为如果指针的值是对象的地址,那么指针就指向该对象,除非标准明确排除这种情况,这是公理。
\n\n这与[basic.compound]/3的文本一致,它说的是相反的,即如果一个指针指向一个对象,那么它的值就是该对象的地址。
\n\n似乎没有任何其他显式语句定义指针何时可以或不能指向对象,但 basic.compound/3 表示所有指针必须是四种情况之一(指向对象、指向对象)超过结尾,空,无效)。
\n\n排除的情况示例包括:
\n\nstd::launder专门解决了存在此类语言排除使用未经清洗的指针的情况。 | 归档时间: |
|
| 查看次数: |
378 次 |
| 最近记录: |