若采纳P1839,能否通过对象表示偏移访问C++子对象?
这是Stack Overflow问题《通过封闭对象地址加字节偏移访问子对象是否属于未定义行为?》的扩展讨论,前提是假设C++提案P1839已被采纳。
struct V2 { float x; float y; float& f() { // 如果有影响,可以用char*或unsigned char*替代std::byte std::byte* self = reinterpret_cast<std::byte*>(this); // (1) std::byte* rep_y = self + offsetof(V2, y); // (2) float* ptr_y = reinterpret_cast<float*>(rep_y); // (3) return *ptr_y; // (4) } };
此前针对这类代码合法性的讨论围绕C++17之后的对象表示检查展开,共识是形式上(2)中的指针算术属于未定义行为(UB)。而P1839的核心目标正是修复对象表示的迭代问题,理论上能让(2)不再是UB,但需要明确嵌套和子对象相关的规范细节,以下针对几个核心问题逐一分析:
1. 无偏移时的reinterpret_cast合法性
是的,指针可互换性规则中“一方是另一方的对象表示或其首元素”的条款,允许在this(V2*)、它的对象表示指针(比如std::byte*)、以及转回的V2*之间进行reinterpret_cast转换,并且支持后续的值访问。
这是因为对象的首字节指针本身就属于对象表示的一部分,符合指针可互换的条件,转换后访问原对象的行为是合法的。
2. rep_y能否视为y的对象表示首元素?
可以。V2的对象表示包含其所有非静态数据成员的对象表示,y作为V2的直接子对象,它的对象表示首元素就是V2对象表示中偏移offsetof(V2, y)的位置。
哪怕这个指针是从V2的对象表示构造出来的,而非直接取&y,只要偏移计算正确(这里用offsetof保证了这一点),rep_y就确实指向y的对象表示首字节,因此通过(3)转换为float*后访问y的值是合法的。
P1839修复了原标准中对象表示迭代的限制,允许在完整对象的对象表示范围内进行指针算术,只要目标位置属于该对象或其子对象的对象表示,所以这里的操作完全符合提案的意图。
3. 如果存在疑问,std::launder能否解决问题?
即使退一步说,假设ptr_y的合法性存在争议,P1839对std::launder的修改也能解决问题。修改后的std::launder允许清洗指向“原始指针可达范围内的对象”的指针,这里y是this指向的V2对象的直接子对象,显然属于可达范围。
不过在这个场景下其实不需要std::launder——因为前面已经确定rep_y就是y的对象表示首元素,转换后的ptr_y本身就是指向y的有效指针,直接访问没有问题。
4. 若以上都不成立,是否有合法替代方案?
在P1839的框架下,上述代码本身就是合法的,完全符合提案修复对象表示迭代问题的精神。如果要更直接的写法,也可以用unsigned char*替代std::byte*(两者在对象表示操作上等价),或者直接合并转换步骤:
float* ptr_y = reinterpret_cast<float*>(reinterpret_cast<std::byte*>(this) + offsetof(V2, y));
本质逻辑完全一致,都是合法的。
内容的提问来源于stack exchange,提问作者LtSten

