这会避免UB吗?

这个问题更像是一个学术问题,因为没有正当理由再编写自己的offsetof宏了。 不过,我已经在这里和那里看到了这个本土实现的弹出窗口:

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

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

C11(ISO / IEC 9899:201x)§6.3.2.3指针第3节

值为0的整型常量表达式或类型为void *的表达式称为空指针常量

所以上面的实现是根据我阅读标准的方式,与写作相同:

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

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

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

看起来,而不是0,1用作指针,我在结尾处减去1,结果应该是相同的。 我不再使用NULL指针了。 据我所知,结果是一样的 。

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

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

缺陷报告#44说:

“特别是,这就是为什么存在offsetof宏的原因:否则没有可移植的方法来计算这样的转换时间常数。”

(DR#44适用于C89,但C99和C11中的语言没有任何改变,允许便携式实现。)

我相信这种行为是实现定义的。 在n1256的6.3.2.3中:

5整数可以转换为任何指针类型。 除非先前指定,否则结果是实现定义的,可能未正确对齐,可能不指向引用类型的实体,并且可能是陷阱表示。

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

6.2.4对象的存储持续时间

  1. 对象的生命周期是程序执行的一部分,在此期间保证为其保留存储。 存在一个对象,具有一个常量地址,33)并在其整个生命周期内保留其最后存储的值。 34) 如果一个对象在其生命周期之外被引用,则该行为是不确定的。 当指针指向(或刚刚过去)的对象到达其生命周期的末尾时,指针的值变得不确定。

J.2未定义的行为
– 使用指向生命周期结束的对象的指针的值(6.2.4)。

3.19.2 indeterminate value:未指定的值或陷阱表示

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

将整数转换为指针也存在问题:

6.3.2.3指针

  1. 整数可以转换为任何指针类型。 除非先前指定,否则结果是实现定义的,可能未正确对齐,可能不指向引用类型的实体,并且可能是陷阱表示。 67)

使用取消引用NULL指针实现offsetof调用未定义的行为。 在该实现中,假设假设结构从地址0开始。 你可能认为它是1 ,是的,它也将调用UB,因为你正在取消引用空指针,但因为未初始化的指针被解除引用。

任何版本的C标准都不会禁止编译器对任何想要实现效果的宏执行任何想要的操作,而无需定义存储位置来保存指定的对象。 尽管如此,forms如下:

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

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

另请注意,“未定义行为”的含义最近已更改。 过去,未定义的行为意味着规范没有说明编译器必须做什么,但大多数编译器通常会选择(有时是任意的)在数学上正确或在底层平台上有意义的行为。 例如,在32位处理器上, int32_t foo=2147483647; foo+=(unsigned char)x; if (foo > 100) ... int32_t foo=2147483647; foo+=(unsigned char)x; if (foo > 100) ... 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 ,则该表达式将始终为true。 给定像offsetof宏这样的代码,无论任何变量的值如何都会产生未定义的行为,编译器有权不仅消除使用它的任何代码,而且还有任何前面的代码,它们不能通过任何定义的方式导致程序执行到终止。

请注意,如果不存在任何已经获取地址并转换为整数的对象,则会将非零整数文字转换为仅指针未定义行为,从而产生相同的值。 因此,编译器将无法识别基于指针差异的offsetof的变体,该变体将一些非零值转换为指针显示未定义的行为,除非它可以确定所讨论的数字与任何指针不对应。 另一方面,尝试将非零整数强制转换为指针会在某些系统上执行validation检查以确保指针有效; 如果不是这样的话,这样的系统就可以陷阱。

你实际上并没有取消引用指针,你正在做的更类似于指针添加,所以使用零应该没问题。