这是否避免了 UB

Does this avoid UB

这个问题更像是一个学术问题,因为没有正当理由再编写自己的 offsetof 宏了。尽管如此,我还是看到了这个本地实现的弹出窗口:

#define offsetof(s, m) ((size_t) &(((s *)0)->m))

从技术上讲,这是取消引用 NULL 指针 (AFAIKT):

C11(ISO/IEC 9899:201x) §6.3.2.3 Pointers Section 3

An integer constant expression with the value 0, or such an expression cast to type void *, is called a null pointer constant

所以上面的实现是,按照我看标准的方式,写的一样:

#define offsetof(s, m) ((size_t) &(((s *)NULL)->m))

我确实想知道,通过更改一个微小的细节,以下 offsetof 的定义将完全合法, 可靠:

#define offsetof(s, m) (((size_t)&(((s *) 1)->m)) - 1)

看来,不是用0,而是用1作为指针,我在最后减1,结果应该是一样的。我不再使用 NULL 指针。据我所知 the results are the same.

所以基本上:在这个 offsetof 定义中使用 1 而不是 0 是否有任何原因可能不起作用?在某些情况下,它是否仍会导致 UB,如果会:何时以及如何?基本上,我在这里要问的是:我在这里遗漏了什么吗?

您实际上并没有取消对指针的引用,您所做的更类似于指针加法,因此使用零应该没问题。

我认为该行为是实现定义的。在 n1256 的 6.3.2.3 中:

5 An integer may be converted to any pointer type. Except as previously specified, the result is implementation-defined, might not be correctly aligned, might not point to an entity of the referenced type, and might be a trap representation.

这两个定义都是未定义的行为:在第一个定义中,一个空指针被取消引用,而在你的第二个定义中,你正在取消引用一个无效指针(指针不指向有效对象)。在 C 中不可能编写 offsetof 宏的可移植版本。

缺陷报告 #44 说:

"In particular, this is why the offsetof macro exists: there was otherwise no portable means to compute such translation-time constants."

(DR#44 适用于 C89,但 C99 和 C11 中的语言没有任何变化,这将允许可移植的实现。)

取消引用 NULL 指针的 offsetof 的实现会调用未定义的行为。在此实现中,假设假设结构从地址 0 开始。您可以假设它是 1,是的,它也会调用 UB,因为您正在取消引用空指针,但因为未初始化的指针被取消引用。

一个问题是您创建的指针没有指向对象。

6.2.4 Storage durations of objects

  1. The lifetime of an object is the portion of program execution during which storage is guaranteed to be reserved for it. An object exists, has a constant address, 33) and retains its last-stored value throughout its lifetime. 34) If an object is referred to outside of its lifetime, the behavior is undefined. The value of a pointer becomes indeterminate when the object it points to (or just past) reaches the end of its lifetime.

J.2 Undefined behaviour
- The value of a pointer to an object whose lifetime has ended is used (6.2.4).

3.19.2 indeterminate value: either an unspecified value or a trap representation

当你将1转换为指针时,创建的指针不指向对象时,指针的值变得不确定。然后使用指针。这两者都会导致未定义的行为。

整数到指针的转换也是有问题的:

6.3.2.3 Pointers

  1. An integer may be converted to any pointer type. Except as previously specified, the result is implementation-defined, might not be correctly aligned, might not point to an entity of the referenced type, and might be a trap representation. 67)

任何版本的 C 标准都不会禁止编译器对任何试图在不定义存储位置来保存指定对象的情况下实现效果的宏做任何它想做的事情。尽管如此,像这样的形式:

#define offsetof(s, m) ((char*)&((((s)*)0)->m)-(char*)0)

对于 C99 之前的编译器,可能非常安全。请注意,它通过从另一个中减去一个 char* 来生成一个整数。当指针访问同一个有效对象的部分时,它被指定工作并产生一个常量值,并且实际上将在任何没有注意到空指针不是有效对象的编译器上工作。相比之下,将指针转换为整数或相反的效果在不同平台上会有所不同,并且有许多平台 (int)(((char*)&foo)+1) - (int)(char*)&foo 可能不会产生 1.

另请注意,"Undefined Behavior" 的含义最近发生了变化。过去,未定义行为意味着规范没有说明编译器必须做什么,但大多数编译器通常会选择(有时是任意的)数学上正确或在底层平台上有意义的行为。例如,在 32 位处理器上,int32_t foo=2147483647; foo+=(unsigned char)x; if (foo > 100) ... 编译器可能会确定对于 x 的任何可能值,分配给 foo 的数学上正确的值将在 2147483647 到 2147483903 的范围内,因此在任何情况下都大于 100。或者,它可能会使用补码算法执行运算,并对可能环绕的值执行比较。然而,较新的编译器可能会做一些更有趣的事情。

新的编译器可能会查看表达式,例如 foo 的示例,并推断如果 x 为零,则 foo 必须保持为 2147483647,如果 x是非零的,编译器将被允许做任何它喜欢的事情,所以它可能会推断出 x 的 LSB 在执行语句时必须等于零,所以如果代码前面有一个测试对于 (unsigned char)x==0,该表达式将始终为真。给定像 offsetof 宏这样的代码,无论任何变量的值如何,它都会生成未定义的行为,编译器将有权消除不仅使用它的任何代码,而且还可以消除任何无法通过任何定义的方式执行的前面的代码导致程序执行终止。

请注意,如果不存在任何对象的地址已被获取并转换为整数以产生相同的值,则将非零整数文字转换为指针仅是未定义行为。因此,编译器将无法识别基于指针差异的 offsetof 宏的变体,该变体将一些非零值转换为指针作为表现出未定义的行为,除非它可以确定所讨论的数字没有对应于任何指针。另一方面,将非零整数转换为指针的尝试在某些系统上会执行验证检查以确保指针有效;如果不是,这样的系统可能会陷入困境。