【问题标题】:How do one use `offsetof` to access a field in a standard conforming way?如何使用`offsetof`以符合标准的方式访问字段?
【发布时间】:2016-09-21 15:02:12
【问题描述】:

假设我有一个结构并将偏移量提取到一个成员:

struct A {
    int x;
};

size_t xoff = offsetof(A, x);

如果给定指向struct A 的指针,我该如何以符合标准的方式提取成员?当然假设我们有一个正确的struct A* 和一个正确的偏移量。一种尝试是:

int getint(struct A* base, size_t off) {
    return *(int*)((char*)base + off); 
}

这可能会起作用,但请注意,例如,如果指针是同一数组的指针(或末尾的指针),则指针算术似乎仅在标准中定义,这不一定是这种情况。因此从技术上讲,该构造似乎依赖于未定义的行为。

另一种方法是

int getint(struct A* base, size_t off) {
    return *(int*)((uintptr_t)base + off);
}

这也可能会起作用,但请注意 intptr_t 不需要存在,据我所知 intptr_t 上的算术不需要产生正确的结果(例如,我记得一些 CPU 有能力处理非字节对齐的地址,这表明intptr_t 在数组中的每个char 以8 的步长增加)。

看起来标准中忘记了一些东西(或者我错过了一些东西)。

【问题讨论】:

  • 我很确定别名为char* 和指向同一个对象(不一定是数组)的指针都是有效的。等待权威答案。
  • (char *)base 可用于在base 内的任意位置移动(并且超过末尾)。任何对象的行为都类似于大小为 1 的数组。
  • return *(int*)((char*)base + off); 很容易失败,因为int 访问可能未对齐。例如。 int 访问可能会导致奇地址上的总线故障。 OTOH OP 确实说过“假设......我们有一个正确的结构 A* 和一个正确的偏移量”
  • 最好访问具有该字段类型或unsigned char(无陷阱,无填充)的字段。
  • 不清楚为什么代码不使用A->x 来访问该字段。 A->x 没有提供的你该怎么做?如果所有代码都是A 并且到字段x 的偏移量,则缺少字段类型/大小会阻止以一致的方式访问。

标签: c pointers language-lawyer offsetof


【解决方案1】:

根据C Standard7.19 通用定义<stddef.h>,第3 段,offsetof() 定义为:

宏是

NULL

扩展为实现定义的空指针常量;和

offsetof(*type*, *member-designator*)

扩展为具有类型的整数常量表达式 size_tvalue 是以字节为单位的偏移量,到 结构成员(由 member-designator 指定),来自 其结构的开头(由 type 指定)。

所以,offsetoff() 返回一个以字节为单位的偏移量。

6.2.6.1 总则,第 4 段指出:

存储在任何其他对象类型的非位域对象中的值 包括 n × CHAR_BIT 位,其中 n 是该类型对象的大小,以字节为单位。

由于 CHAR_BIT 定义为char 中的位数,因此char 是一个字节

所以,按照标准,这是正确的:

int getint(struct A* base, size_t off) {
    return *(int*)((char*)base + off); 
}

这会将base 转换为char * 并将off 字节添加到地址中。如果offoffsetof(A, x);的结果,那么结果地址就是base指向的structure A内的x的地址。

你的第二个例子:

int getint(struct A* base, size_t off) {
    return *(int*)((intptr_t)base + off);
}

取决于有符号的intptr_t 值与无符号的size_t 值相加的结果。

【讨论】:

  • 引用的部分完全不相关。标准的相关部分将是 6.5 中关于指针别名的部分,或者可能是关于指针算术的部分。我看不出第二个例子会如何失败。 intptr_t 是无符号整数类型,不是指针类型。它不做任何指针运算,所以你的假设是不正确的。
  • @Lundin - 是的,你是对的。出于某种原因,我将intptr_t 读作int *。现在修改答案,但首先我需要考虑如果intptr_t 被签名会发生什么。
  • 同意@Lundin,除了intptr_t 是有符号整数类型与uintptr_t 相比
  • @chux - 是的,它已签名。但我试图记住添加有符号和无符号整数时会发生什么。 OP 的代码可能是有效的。
  • OP 似乎知道 offsetof 的工作原理。这里的主要问题是您是否可以对结构和指针转换进行指针运算。关于隐式转换规则:不管你喜欢与否,它们都在那里。不了解它们可能会导致您意外编写依赖它们的代码。
【解决方案2】:

标准 (6.5.6) 仅允许对数组进行指针运算的原因是结构可能具有填充字节以满足对齐要求。因此,在结构中进行指针运算确实是形式上未定义的行为。

实际上,只要您知道自己在做什么,它就会起作用。 base + off 不会失败,因为我们知道那里有有效的数据并且没有错位,只要它被正确访问。

因此(intptr_t)base + off 确实是更好的代码,因为不再有任何指针运算,而只是简单的整数运算。因为intptr_t 是整数,所以它不是指针。

正如评论中指出的,这种类型不保证存在,根据 7.20.1.4/1,它是可选的。我想为了获得最大的可移植性,您可以切换到 保证存在的其他类型,例如 intmax_tptrdiff_t。然而,如果一个不支持intptr_t 的 C99/C11 编译器是否有用,则值得商榷。

(这里有一个小类型问题,即intptr_t 是有符号类型,不一定与size_t 兼容。您可能会遇到隐式类型提升问题。如果可能,使用uintptr_t 会更安全。 )

接下来的问题是*(int*)((intptr_t)base + off) 是否是明确定义的行为。关于指针转换的标准部分(6.3.2.3)说:

任何指针类型都可以转换为整数类型。除了作为 之前指定的,结果是实现定义的。如果 结果不能用整数类型表示,行为是 不明确的。结果不必在任何值的范围内 整数类型。

对于这种特殊情况,我们知道那里有一个正确对齐的int,所以没问题。

(我也不认为任何指针别名问题都适用。至少使用gcc -O3 -fstrict-aliasing -Wstrict-aliasing=2 编译不会破坏代码。)

【讨论】:

  • "因为 intptr_t 是一个整数,所以 ... 保证存在 ... 编译器 (C99/C11)" --> "intptr_t ... uintptr_t 这些类型是 可选。” §7.20.1.4 1
  • @chux 啊,那我学到了一些新东西! :) 将编辑答案,谢谢。
猜你喜欢
  • 2013-11-25
  • 1970-01-01
  • 2019-07-11
  • 2014-02-18
  • 1970-01-01
  • 2023-03-16
  • 2016-06-02
  • 2011-03-28
  • 1970-01-01
相关资源
最近更新 更多