头文件中的缩写struct重新定义是否符合标准?

时间:2018-01-22 21:48:36

标签: c language-lawyer

这与我正在处理的一些旧的(C89之前的)代码有关。该代码的一部分是一个小型库,它在头文件中定义了这些结构:

// lib.h

struct data_node {
  const struct data_node *next;
  const struct data_node *prev;
  void *data;
};

struct trace_node {
  const struct trace_node *next;
  const struct trace_node *prev;
  unsigned int id;
  const char *file;
  int line;
};

const struct trace_node *get_trace(void);

源文件重新定义了相同的结构,如下所示:

// lib.c
// does *not* include "lib.h"

struct data_node {
  struct data_node *next;
  struct data_node *prev;
  void *data;
};

struct trace_node {
  struct trace_node *next;
  struct trace_node *prev;
  unsigned int id;
  const char *file;
  int line;
  struct data_node *syncData; /* not included in header file version */
};

它的工作方式与您期望的一样:syncData字段对包含" lib.h"的客户端代码不可见。头。

背景

该库维护着2个内部列表:跟踪列表和数据列表。 syncData字段使2个列表保持同步(如图所示)。

如果客户端代码有权访问syncData字段,则可能会中断列表之间的同步。但是,跟踪列表可能会变得非常大,因此它不是将每个节点复制到较小版本的结构中,而只是返回内部列表的sentinel节点的地址。

问题

我用-Wall,-Wpedantic和-Wextra编译了这个,我无法通过-std = c99和-std = c11让gcc抱怨它。内存的十六进制转储显示隐藏字段的字节,就在它们应该的位置。

标准(6.2.7.1)的相关部分说:

  

如果类型相同,则两种类型具有兼容类型。附加规则   确定两种类型是否兼容在6.7.2中描述了类型说明符,   在6.7.3中用于类型限定符,在6.7.5中用于声明符.46)此外,有两个结构,   union或在单独的翻译单元中声明的枚举类型如果它们是相容的   标签和成员满足以下要求:如果使用标签声明一个,则   其他声明应使用相同的标签声明。如果两者都是完整类型,则以下内容   附加要求:他们之间应有一对一的对应关系   成员,使每对相应的成员声明兼容   类型,如果相应对的一个成员用名称声明,则   其他成员使用相同的名称声明。对于两个结构,相应的   成员应按同一顺序宣布。对于两个结构或联合,对应   位域应具有相同的宽度。对于两个枚举,相应的成员   应具有相同的值。

根据您的阅读方式,可以认为兼容的结构定义仅限于 ONLY 具有相应的成员对(并且没有其他成员),或者结构定义是兼容如果,他们确实有相应的成员对,那些对符合要求。

我不认为这是未定义的行为。在最坏的情况下,我认为可能没有具体说明。我应该重构这个以使用2个不同的结构定义吗?执行此操作需要在性能损失中为内部列表中的每个节点分配新的公共节点,并复制公共数据。

4 个答案:

答案 0 :(得分:3)

这有不确定的行为,并且有充分的理由。

首先,文本明确指出兼容的struct必须在字段之间具有一对一的对应关系。因此,如果客户端和库访问同一对象,则行为未定义。编译器无法检测到未定义的行为,因为此规则是将来自两个单独编译的不同转换单元的知识结合在一起。这就是您没有看到任何诊断的原因。

你的例子特别糟糕的原因是,即使两个struct类型的大小也没有协同作用,甚至可能没有对齐。因此,访问此类对象的客户端将对优化机会做出错误的假设。

答案 1 :(得分:0)

如果lib.c不包含lib.h,那么lib.c中的定义不可见,因此没有冲突。

C不使用函数重载,所以它不使用名称修改,所以如果你有这个声明:

struct trace_node *get_trace(void);

在一个地方,但功能实现为

struct foo_trace_node *get_trace(void);

然后链接器会愉快地将您的代码与get_trace()

链接起来

答案 2 :(得分:0)

您所做的是直接违反标准。 “一对一的对应”期望指针也被客户端代码看到。您的代码违反了该句子的第一部分。

想象一下客户端代码将一个结构从列表中取消链接,并在一个“他的”结构中创建并链接错误的大小 - 在取消引用该指针时,Lib迟早会崩溃。

如果您不想暴露某些结构字段,请不要暴露结构。将客户端代码交给匿名结构指针,并在lib中公开访问器函数,返回允许客户端看到的字段值。或者将“允许的部分”打包到较大的嵌入式结构中,如果要避免逐个字段访问,请将其移交给客户端代码。让客户端代码看到列表结构的链接指针可能也不是一个好主意。

答案 3 :(得分:0)

我在

中找到了一些其他信息
  

新C标准
  经济文化评论   德里克·M·琼斯   derek@knosof.co.uk
  http://www.coding-guidelines.com/cbook/cbook1_2.pdf

这个特定的版本涵盖了C99标准,但由于相关部分的文本在两个版本的标准中都是相同的,所以它是一种清洗。

相关评论:

  

633
  此外,声明了两个结构,联合或枚举类型   单独的翻译单元如果他们的标签和成员是兼容的   满足以下要求:

     

评论

     

如果声明了结构或联合类型,则这些要求适用   通过typedef或通过任何其他方式。因为可以有更多   这些是同一翻译单元中的一种类型的声明   要求确实适用于每个翻译中的复合类型   单元。在以下要求列表中,仅适用于那些要求   结构,联合,枚举或其组合   明确地这样称呼。如果他们服从,两种类型是兼容的   以下两个要求:

     

•标签兼容性。

     
      
  1. 如果两种类型都有标签,则两者都应相同。
  2.   
  3. 如果一个或两个类型都没有标签,则无需遵守。
  4.         

    •会员兼容性。

         

    这里的要求是两种类型的每个成员都有一个   具有以下其他类型的相应成员   属性:

         
        
    1. 相应的成员具有兼容的类型。
    2.   
    3. 相应的成员具有相同的名称或未命名。
    4.   
    5. 对于结构类型,相应的成员在各自的定义中按相同的顺序定义。
    6.   
    7. 对于结构和联合类型,相应的成员要么都是具有相同宽度的位字段,要么都不是   位字段。
    8.   
    9. 对于枚举类型,相应的成员(枚举常量)具有相同的值。
    10.   

所以判决是一致的。琼斯先生评论中的措辞可能更清楚一点(对我来说,无论如何)。

我在OP中没有提到get_trace()函数的原始头文件注释清楚地表明跟踪列表被认为是严格只读的,因此提出了关于缩写结构的对象的观点在客户端代码中寻找回到库代码的方式 - 虽然在一般意义上仍然有效 - 但在这种特定情况下并不完全适用。

但是,编译器优化的问题仍在继续,特别是考虑到现在有多么激进的编译器优化,而不是35年前。所以,我会重构。