在结构的末尾填充以实现未来的增长

时间:2019-09-15 18:15:09

标签: c struct

乌尔里希·德雷珀(Ulrich Drepper)将此document称为“图书馆设计,实施和维护的良好实践”(第5页底部):

  

[...]类型定义应始终至少创建一个最小值   允许未来增长的填充量

     

[...]

     

第二,结构的末尾应包含一定数量的填充字节。

for (unsigned char k = 0; k < 255; k++)
     

[...]

     

如果以后必须将字段添加到结构中,则可以将类型定义更改为此:

struct the_struct
{
  int foo;
  // ...and more fields
  uintptr_t filler[8];
};

我看不到在结构末尾添加此填充符的意义。是的,这意味着在向结构中添加新字段(struct the_struct { int foo; // ...and more fields union { some_type_t new_field; uintptr_t filler[8]; } u; }; )时,它实际上并没有增长。但是,将新字段添加到您不知道会需要的结构中不是全部意思吗?在此示例中,如果要添加一个字段而不是20个字段怎么办?然后,您是否应该使用1k字节的填充符以防万一?此外,为什么在库的后续版本中不更改结构的大小很重要?如果库提供干净的抽象,那应该没关系吧?最后,使用64字节的填充符(8 uintpr_t(是的,不一定是64字节))听起来像是浪费内存...

该文档完全不涉及此细节。您对这种建议“在结构末尾添加填充剂以计划未来的增长”为什么是一个好建议有何解释?

2 个答案:

答案 0 :(得分:2)

是的,根据情况,结构的大小对于二进制兼容性可能很重要。

考虑stat()。通常这样称呼:

struct stat stbuf;
int r = stat(filename, &stbuf);

使用此设置,如果stat结构的大小发生变化,则每个调用者都将变为无效,并且需要重新编译。如果被调用方代码和调用方代码都是同一项目的一部分,那可能不是问题。但是,如果(例如stat(),这是对Unix / Linux内核的系统调用)那里有很多调用者,那么实际上就不可能强迫它们全部重新编译,因此这意味着stat结构的大小永远无法更改。

这类问题主要是在调用方分配(或检查/操纵)结构的实际实例时出现的。另一方面,如果仅通过库代码分配和操作结构的内部-如果调用代码仅处理指向该结构的指针,并且不尝试解释所指向的结构,则可能结构是否变化都没关系。

(现在,尽管如此,如果结构必须更改大小,还有许多其他方法可以缓解问题。有些库中调用方分配了结构实例,但随后又传递了两个指向结构的指针,以及调用者知道的结构的 size ,直到库代码,新的库代码可以检测到不匹配,并避免设置或使用较旧的调用者使用的新字段我不相信gcc至少实现了特殊的钩子,以便glibc可以实现同一结构的多个版本,以及使用它们的库函数的多个版本,以便可以使用正确的库函数。例如,回到stat(),例如在Linux下,至少有两个stat结构的不同版本,其中一个分配32位用于文件大小和一个可分配64的文件。)

答案 1 :(得分:1)

  

但是将新字段添加到结构中并不是您要说的全部内容   不知道您会需要它们吗?

是的,如果您一直都知道需要这些成员,那么故意省略它们会适得其反。但是有时您确实确实发现以后才需要一些其他字段。 Drepper的建议介绍了设计代码的方法-特别是结构定义-以便您添加具有最小可能副作用的成员。

  

在此示例中,如果您   是否要添加一个字段而不是20个字段?

您不会一开始就说“我要添加20个成员”。相反,您开始说“我以后可能会发现需要更多成员”。这是一个谨慎的立场。

  

然后您应该使用1k的填充符   个字节以防万一?

这是一个判断电话。我认为,在大多数情况下,结构定义中的KB多余空间可能会过大,但是在某些情况下这是合理的。

  

此外,为什么重要的是   结构在后续版本的库中不会更改?如果   库提供干净的抽象,这应该没关系吗?

保持大小不变​​有多重要是一个主观的问题,但是大小确实与共享库的二进制兼容性有关。具体来说,问题是我是否可以删除共享库的新版本来代替旧版本,并希望现有程序可以与新版本一起使用而无需重新编译。

从技术上讲,如果结构的定义发生变化,即使其大小没有变化,那么就C语言而言,新的定义也与旧的定义不兼容。但是,实际上,对于大多数C实现,如果结构大小相同并且布局仅在以前未使用的空间内不发生变化,则现有用户将不会注意到许多操作之间的差异。

但是,如果大小改变,则

  • 动态分配结构实例将不会分配正确的空间量。
  • 该结构的数组布局不正确。
  • 通过memcpy()从一个实例复制到另一个实例将无法正常工作。
  • 涉及结构实例的二进制I / O不会传输正确数量的字节。

在将某些尾随填充转换为有意义的成员后,尺寸更改可能(在实践中)仍然可以解决其他问题。

请注意:如果结构成员在不改变整体大小的情况下进行更改,那么可能仍然是一个问题,那就是按值将结构传递给函数,并(以较小的程度)将它们作为返回值接收。使用这种方法提供二进制兼容性的库最好避免提供执行这些操作的函数。

  

最后,使用64个字节的填充符(8 uintpr_t(是,不是   一定是64个字节))听起来像是在浪费内存...

在实际上每个结构64个字节确实值得关注的情况下,那么这可能会超过二进制兼容性的关注。如果您希望同时使用大量这些结构,或者您的内存受到极大限制,那就是这种情况。但是,在许多情况下,多余的空间是无关紧要的,而通过包含填充提供的二进制兼容性的额外范围却非常有价值。

  

该文档完全不涉及此细节。你会   对于此建议“为什么要在末尾添加填充剂”有任何解释   计划未来增长的结构”是一个好人吗?

与大多数情况一样,需要根据您的特定上下文评估推荐。在前面的内容中,我谈到了您希望在评估中要考虑的大多数问题。