C ++ 17(expr.add/4)说:
添加或减去具有整数类型的表达式时 从指针开始,结果具有指针操作数的类型。如果 表达式P指向具有n的数组对象x的元素x [i] 元素,表达式P + J和J + P(其中J的值为j) 如果0≤i+j≤n,则指向(可能是假设的)元素x [i + j]; 否则,行为未定义。同样,表达式P-J 如果0≤i-j≤n,则指向(可能是假设的)元素x [i-j]; 否则,行为未定义。
struct Foo {
float x, y, z;
};
Foo f;
char *p = reinterpret_cast<char*>(&f) + offsetof(Foo, z); // (*)
*reinterpret_cast<float*>(p) = 42.0f;
该行标有(*)UB吗? reinterpret_cast<char*>(&f)
不指向char数组,而是指向float,因此根据引用的段落它应该是UB。但是,如果它是UB,那么offsetof
的用处将是有限的。
是UB吗?如果没有,为什么不呢?
答案 0 :(得分:6)
此添加旨在有效,但我不相信该标准设法说得那么清楚。引用N4140(大致是C ++ 14):
3.9类型[basic.types]
2对于普通可复制类型
T
的任何对象(基类子对象除外),无论对象是否包含类型T
的有效值,基础字节(1.7)都会生成向上对象可以复制到数组中char
或unsigned char
。 42 [...]42)例如,使用库函数(17.6.1.2)
std::memcpy
或std::memmove
。
它说&#34;例如&#34;因为std::memcpy
和std::memmove
不是允许复制基础字节的唯一方法。手动复制的简单for
循环也应该是有效的。
为了使其工作,必须为构成对象的原始字节的指针定义加法,并且表达式的定义方式有效,加法的定义不能取决于是否添加&#39 ; s结果随后将用于将字节复制到数组中。
这是否意味着那些字节已经形成一个数组,或者这是否是+
运算符的一般规则的特殊例外,在运算符描述中以某种方式被省略,我不清楚(我怀疑前者) ),但无论哪种方式都会使您在代码中执行的添加有效。
答案 1 :(得分:3)
任何不允许offsetof
使用的解释都必须是错误的:
#include <assert.h>
#include <stddef.h>
struct S { float a, b, c; };
const size_t idx_S[] = {
offsetof(struct S, a),
offsetof(struct S, b),
offsetof(struct S, c),
};
float read_S(struct S *sp, unsigned int idx)
{
assert(idx < 3);
return *(float *)(((char *)sp) + idx_S[idx]); // intended to be valid
}
但是,任何允许人们超越显式声明的数组结尾的解释也必须是错误的:
#include <assert.h>
#include <stddef.h>
struct S { float a[2]; float b[2]; };
static_assert(offsetof(struct S, b) == sizeof(float)*2,
"padding between S.a and S.b -- should be impossible");
float read_S(struct S *sp, unsigned int idx)
{
assert(idx < 4);
return sp->a[idx]; // undefined behavior if idx >= 2,
// reading past end of array
}
我们现在处于两难境地,因为C和C ++标准中的措辞,旨在禁止第二种情况,可能也不允许第一种情况。
这通常被称为“什么是对象?”问题。自20世纪90年代以来,人们,包括C和C ++委员会的成员,一直在争论这个问题和相关问题,并且已经多次尝试修复措辞,据我所知,没有人成功(在所有意义上现有的“合理”代码绝对符合要求,并且仍允许所有现有的“合理”优化。
(注意:以上所有代码都是用C语言编写的,以强调两种语言都存在相同的问题,并且可以在不使用任何C ++构造的情况下遇到。)
答案 2 :(得分:1)
据我所知,您的代码有效。根据§3.10¶10.8明确允许将对象别名为char
数组:
如果程序试图通过以下类型之一以外的glvalue访问对象的存储值,则行为未定义:
- [...]
char
或unsigned char
类型。
另一个问题是,是否将char*
指针强制转换回float*
并通过它指定是有效的。由于您的Foo
是POD类型,因此可以。您可以计算POD成员的地址(假设计算本身不是UB),然后通过该地址访问该成员。例如,您不得滥用此权限来访问非POD对象的private
成员。此外,如果您倾向于转换为int*
或写入不存在float
类型的对象的地址,那么它将是UB。这背后的原因可以在上面引用的部分找到。
答案 3 :(得分:1)
是的,这是不确定的。如您在问题中所述,
reinterpret_cast<char*>(&f)
不是指向char数组,而是指向浮点数,...
... reinterpret_cast<char*>(&f)
does even not point to a char,因此即使对象表示形式为char数组,其行为仍未定义。
对于offsetof
,您仍然可以像
struct Foo {
float x, y, z;
};
Foo f;
auto p = reinterpret_cast<std::uintptr_t>(&f) + offsetof(Foo, z);
// ^^^^^^^^^^^^^^
*reinterpret_cast<float*>(p) = 42.0f;
答案 4 :(得分:1)
请参见CWG 1314
根据6.9 [basic.types]第4段,
类型T的对象的对象表示形式是由类型T的对象占用的N个无符号字符对象的序列,其中N等于sizeof(T)。
和4.5 [介绍对象]第5段,
普通可复制或标准布局类型(6.9 [basic.types])的对象应占据连续的存储字节。
这些段落是否使标准布局对象中的指针算术运算(8.7 [expr.add]第5段)得到了明确定义(例如,用于编写自己的memcpy版本)?
理论值(2011年8月):
当前的措辞足够清楚,表明可以使用这种用法。
我强烈不同意CWG的说法,即“当前措词已经足够清楚”,但这是我们的裁定。
我将CWG的响应解释为,建议将指向unsigned char
的指针转换为普通可复制或标准布局类型的对象,以进行指针算术,应将其解释为指向{{ 1}}的大小等于所讨论对象的大小。我不知道他们是否打算使用unsigned char
指针或char
指针(从C ++ 17开始)也可以使用它。 (也许如果他们决定进行实际澄清,而不是声称现有措辞足够清楚,那么我会知道答案的。)
(一个单独的问题是是否需要std::byte
才能使OP的代码定义正确。我在这里不做介绍;我认为这值得一个单独的问题。)