gez*_*eza 12 c++ language-lawyer
C++ 17(expr.add/4)说:
当向指针添加或从指针中减去具有整数类型的表达式时,结果具有指针操作数的类型.如果表达式P指向具有n个元素的数组对象x的元素x [i],则表达式P + J和J + P(其中J具有值j)指向(可能是假设的)元素x [i + j]如果0≤i+j≤n; 否则,行为未定义.同样地,如果0≤i-j≤n,则表达式P-J指向(可能是假设的)元素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;
Run Code Online (Sandbox Code Playgroud)
该行标有(*)UB?reinterpret_cast<char*>(&f)不指向char数组,而是指向浮点数,因此根据引用的段落它应该是UB.但是,如果它是UB,那么它offsetof的用处将是有限的.
是UB吗?如果没有,为什么不呢?
小智 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.
它说"例如"因为std::memcpy并且std::memmove不是允许复制基础字节的唯一方法.for手动逐字节复制的简单循环也应该是有效的.
为了使其工作,必须为构成对象的原始字节的指针定义加法,并且表达式的定义方式有效,加法的定义不能取决于加法的结果是否随后将用于复制字节成阵列.
这是否意味着那些字节已经形成一个数组,或者这是否是+运算符的一般规则的特殊例外,在运算符描述中以某种方式被省略,我不清楚(我怀疑前者),但无论哪种方式都会使另外,您在代码中的表现有效.
任何不允许预期用途的解释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
}
Run Code Online (Sandbox Code Playgroud)
然而,任何允许越过显式声明的数组末尾的解释也一定是错误的:
#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
}
Run Code Online (Sandbox Code Playgroud)
我们现在正处于进退两难的境地,因为 C 和 C++ 标准中的措辞本来是不允许第二种情况的,但很可能也不允许第一种情况。
这就是俗称的“什么是对象?” 问题。自 20 世纪 90 年代以来,人们,包括 C 和 C++ 委员会的成员,一直在争论这个问题和相关问题,并且已经多次尝试修复措辞,但据我所知,没有一个成功(从某种意义上说,所有现有的“合理”代码被渲染为绝对一致,并且仍然允许所有现有的“合理”优化)。
(注:以上所有代码均按照 C 语言编写,以强调两种语言中都存在相同的问题,并且无需使用任何 C++ 结构即可遇到该问题。)
参见CWG 1314
根据 6.9 [basic.types] 第 4 段,
T 类型对象的对象表示是 T 类型对象占用的 N 个 unsigned char 对象的序列,其中 N 等于 sizeof(T)。
和 4.5 [intro.object] 第 5 段,
普通可复制或标准布局类型(6.9 [basic.types])的对象应占用连续的存储字节。
这些段落是否使标准布局对象中的指针算术(8.7 [expr.add] 第 5 段)得到明确定义(例如,用于编写自己的 memcpy 版本?)
理由(2011 年 8 月):
目前的措辞足够明确,允许这种用法。
我强烈不同意 CWG 的“目前的措辞足够明确”的说法,但尽管如此,这就是我们的裁决。
我将 CWG 的响应解释为建议,unsigned char出于指针算术的目的,指向普通可复制或标准布局类型的对象的指针应解释为指向其unsigned char大小等于所讨论对象的大小的数组的指针。我不知道他们是否希望它也可以使用char指针或(从 C++17 开始)std::byte指针来工作。(也许如果他们决定真正澄清它而不是声称现有的措辞足够清楚,那么我就会知道答案。)
(一个单独的问题是是否std::launder需要使OP的代码定义良好。我不会在这里讨论这个问题;我认为它值得一个单独的问题。)
| 归档时间: |
|
| 查看次数: |
392 次 |
| 最近记录: |