使用stddef.h的offsetof而不是滚动你自己的可移植性

这是一个挑剔的细节问题,有三个部分。 上下文是我希望说服一些人无条件地使用offsetof定义而不是(在某些情况下)滚动自己的定义是安全的。 有问题的程序完全用普通的旧C编写,所以在回答时请完全忽略C ++

第1部分:当以与标准offsetof相同的方式使用时,此宏的扩展是否会引发每个C89的未定义行为,为什么或为什么不这样,并且它在C99中是不同的?

 #define offset_of(tp, member) (((char*) &((tp*)0)->member) - (char*)0) 

注意:人们感兴趣的所有实现都取代了标准的规则,指针只有在指向同一个数组时才能相互减去,通过定义所有指针,无论类型或值,指向单个指针全球地址空间。 因此,在争论这个宏的扩张引发未定义的行为时,请不要依赖于该规则。

第2部分:据您所知,有没有一个已发布的生产C实现,当进行上述宏的扩展时,(在某些情况下)将表现出与使用其offsetof宏时的行为不同的行为代替?

第3部分:据您所知,最近发布的生产C实现是什么,它没有提供stddef.h或者没有在该头文件中提供offsetof的工作定义? 该实现是否声称符合任何版本的C标准?

对于第2部分和第3部分,请仅在您可以命名特定实现并给出其发布日期时回答。 说明可能符合条件的实现的一般特征的答案对我没用。

回答#2:是的,gcc-4 *(我现在看的是2009年8月4日发布的v4.3.4,但它应该适用于迄今为止所有的gcc-4版本)。 在stddef.h中使用以下定义:

 #define offsetof(TYPE, MEMBER) __builtin_offsetof (TYPE, MEMBER) 

其中__builtin_offsetof是一个内置像sizeof的编译器(也就是说,它没有实现为宏或运行时函数)。 编译代码:

 #include  struct testcase { char array[256]; }; int main (void) { char buffer[offsetof(struct testcase, array[0])]; return 0; } 

使用你提供的宏的扩展会导致错误(“数组’缓冲区的大小’不是一个整数的常量表达式”)但是在使用stddef.h中提供的宏时会起作用。 使用gcc-3构建使用类似于你的宏。 我想gcc开发人员对这里已经表达的未定义行为等有很多相同的关注,并且创建了内置的编译器,作为尝试在C代码中生成等效操作的更安全的替代方法。

附加信息:

  • 来自Linux内核开发人员列表的邮件列表线程
  • GCC 关于offsetof的文档
  • 这个网站上的一个相关问题

关于你的其他问题:我认为就问题#1而言,R的回答和随后的评论很好地概述了标准的相关部分。 至于你的第三个问题,我还没有听说过没有stddef.h现代 C编译器。 我当然不会认为任何编译器缺少像“生产”那样的基本标准头。 同样,如果它们的offsetof实现不起作用,那么编译器在被认为是“生产”之前仍然有工作要做,就像stddef.h中的其他东西(如NULL )不起作用一样。 在C标准化之前发布的AC编译器可能没有这些东西,但ANSI C标准已有20多年的历史,所以你不太可能遇到其中的一个。

这个问题的整个前提提出了一个问题:如果这些人确信他们不能信任编译器提供的offsetof版本,那么他们可以信任什么呢? 他们是否相信NULL定义正确? 他们是否相信long int不小于常规int ? 他们是否相信memcpy像它应该的那样工作? 他们是否推出了自己的C标准库function的其余版本? 拥有语言标准的一个重要原因是,您可以信任编译器正确执行这些操作。 信任编译器除了offsetof以外的其他一切似乎很愚蠢。

更新:(回应您的意见)

我认为我的同事表现得像你这样:-)我们的一些旧代码仍然有自定义宏定义NULLVOID和其他类似的东西,因为“不同的编译器可能以不同的方式实现它们”(叹气)。 其中一些代码是在C标准化之前写回来的,许多老开发人员仍然处于这种心态,即使C标准明确说明不是这样。

你可以做的一件事就是certificate他们错了,同时让每个人都开心:

 #include  #ifndef offsetof #define offsetof(tp, member) (((char*) &((tp*)0)->member) - (char*)0) #endif 

实际上,他们将使用stddef.h提供的版本。 但是,如果您遇到未定义它的假设编译器,则自定义版本将始终存在。

基于我多年来的类似对话,我认为offsetof不属于标准C的信念来自两个地方。 首先,它是一个很少使用的function。 开发人员不经常看到它,所以他们忘记它甚至存在。 其次,在Kernighan和Ritchie的开创性着作“The C Programming Language” (即使是最新版本)中也没有提到offsetof 。 本书的第一版是在C标准化之前的非官方标准,我经常听到人们错误地将该书称为该语言的标准。 它比官方标准更容易阅读,所以我不知道我是否因为把它作为第一个参考点而责怪它们。 然而,无论他们认为什么,标准都清楚, offsetof是ANSI C的一部分(参见R的链接答案)。


这是另一种看问题#1的方法。 ANSI C标准在4.1.5节中给出了以下定义:

  offsetof( type, member-designator) 

它扩展为一个整数常量表达式,其类型为size_t,其值是以字节为单位的偏移量,从结构的开头(由类型指定)到结构成员(由mem​​ber-designator指定)。

使用offsetof宏不会调用未定义的行为。 实际上,行为就是标准实际定义的所有行为。 由编译器编写者来定义offsetof宏,使其行为遵循标准。 无论是使用宏,内置编译器还是其他东西来实现,确保它按预期运行,都需要实现者深入理解编译器的内部工作方式以及它将如何解释代码。 编译器可以使用宏来实现它,就像你提供的惯用版本一样,但这只是因为他们知道编译器将如何处理非标准代码。

另一方面,您提供的宏扩展确实会调用未定义的行为。 由于您对编译器的了解不足以预测它将如何处理代码,因此无法保证offsetof特定实现始终有效。 许多人定义了他们自己的版本,并没有遇到问题,但这并不意味着代码是正确的。 即使这是特定编译器定义offsetof ,自己编写代码也会调用UB,而使用提供的offsetof宏则不会。

如果不调用未定义的行为,则无法完成滚动自己的offsetof (ANSI C部分A.6.2“未定义的行为”,第27个项目符号点)。 使用stddef.hoffsetof版本将始终产生标准中定义的行为(假设符合标准的编译器)。 我建议不要定义自定义版本,因为它可能导致可移植性问题,但如果其他人无法说服,那么上面提供的#ifndef offsetof片段可能是一个可接受的折衷方案。

无法编写便携式offsetof宏。 您必须使用stddef.h提供的那个。

关于您的具体问题:

  1. 宏调用未定义的行为。 除非它们指向同一个数组,否则不能减去指针。
  2. 实际行为的最大区别在于宏不是一个整型常量表达式 ,因此它不能安全地用于静态初始化器,位域宽度等。严格的bounds-checking-type C实现也可能完全破坏它。
  3. 从来没有任何C标准缺少stddef.hoffsetof 。 前ANSI编译器可能缺乏它,但它们有更多的基本问题,使它们无法用于现代代码(例如缺少void *const )。

而且,即使一些理论编译器确实缺少stddef.h ,你也可以提供一个替代品,就像人们放入stdint.h以便与MSVC一起使用…

(1)在进行减法之前,已经存在未定义的行为。

  1. 首先, (tp*)0不是你想象的那样。 它是一个空指针 ,这样的野兽不一定用全零位模式表示。
  2. 然后成员运算符->不仅仅是偏移量加法。 在具有分段内存的CPU上,这可能是一个更复杂的操作。
  3. 如果表达式不是有效对象,则使用&操作获取地址为UB。

(2)对于第2点,在使用分段存储器的野外(嵌入式东西)中肯定存在仍然存在的结构。 对于3.,R对整数常量表达式的要点有另一个缺点:如果代码被严重优化,则& operation可能在运行时完成并发出错误信号。

(3)从未听说过这样的事情,但这可能不足以为你的同事提供帮助。

我相信几乎每个优化编译器都会在多个时间点打破该宏。 你的同事显然很幸运,没有受到它的打击。

会发生什么事情是一些初级编译工程师决定,因为零页面永远不会映射到他们选择的平台上,任何时候任何人用指向该页面的指针做任何事情,这是未定义的行为,他们可以安全地优化整个表达式。 在这一点上,每个人的自制软件都会破坏,直到有足够多的人对它进行尖叫,我们这些足够聪明的人不会对我们的业务感到高兴。

我不知道任何编译器,这是当前发布版本中的行为,但我想我已经看到它发生在我曾经使用过的每个编译器的某个时刻。