为什么实现文件中的自由函数默认没有内部链接?

Tag*_*gor 16 c c++

当涉及到函数(C++ 中的非成员函数)时,将它们标记为静态会赋予它们内部链接。这意味着它们在翻译单元之外不可见。为什么这不是默认值?我没有很好的统计数据,但从我所看到的实现文件中的大多数函数应该标记为静态。

我相信共识是​​将功能分割成更小的单元。因此,一般来说,实现文件中不应该在其他翻译单元中可见的“实用程序”类函数的数量大于只是公共接口的实现的函数的数量,这是有道理的。

在这种情况下,他们默认选择“导出所有内容”的原因是什么?

Hol*_*Cat 17

在 C/C++ 编译模型中,预处理器在其他所有内容之前运行,并将#includes 替换为它们的内容。

因此,.cpp 文件中定义的函数与其包含的标头中定义的函数之间没有区别。

您的建议将默认在标头中定义函数static(这将消除“多重定义”链接错误),这将非常糟糕,因为如果您忘记inline(在 C++ 中)或者如果您不知道你不应该在头文件中定义函数(在 C 中)。

  • @user694733我在第二段中解决了这个问题。无法区分 .cpp 文件中定义的函数和它包含的标头中定义的函数。 (13认同)
  • 早期的编程语言,包括早期版本的 C,没有不同类型的链接。函数外部的每个标识符都链接在一起。现在称为外部链接。内部链接是后来发明的,默认情况下在函数外部声明的标识符已经具有外部链接。 (5认同)
  • 但这是 C 设计者默认选择外部链接的真正原因吗?如果不是,那么我认为这不能回答问题*“为什么?”*。 (4认同)
  • @chqrlie 我不明白。在我描述的场景中,根本没有原型。想象一下,我将以下内容添加到标头:`void foo() {std::cout << "Hello!\n";}`。根据当前规则,这是一个链接错误。如果您将其隐式设置为“静态”(没有原型),则这不再是错误,而是二进制文件中的静默代码重复。 (3认同)

Ser*_*sta 11

  1. C部分

    C 现在是一种非常古老的语言(从 20 世纪 70 年代开始......)并且非常保守。包含文件只是为了包含源代码级别。C11 的 n1570 草案明确指出:

    源文件以及通过预处理指令 #include 包含的所有标头和源文件一起称为预处理翻译单元。经过预处理后的预处理翻译单元称为翻译单元。

    这意味着一致的 C 编译器不会对包含文件和源文件的内容产生任何差异,因为包含发生在编译阶段之前。

    这足以让函数默认接收外部链接(未声明为static)。

  2. C++部分

    尽管是一种完全不同的语言,C++ 仍然继承自 C。具体来说,C 标准库仍然是 C++ 标准库的正式一部分。

    这对于非成员函数来说可能足以在默认情况下接收与它们在 C 中接收的相同处理。这当然远没有 C 语言中那么重要,因为 C 函数实际上被声明为extern C. 但另一方面,非成员函数也被称为名称空间作用域函数是有原因的。而在C++中,作用域是处理命名空间污染的正确方法。

我的观点是,最佳实践应该建议对一切进行范围分析。您只需使用命名范围来获取外部链接,并使用匿名范围来将范围限制为本地单元。这足以不需要更改非成员函数的 C 默认值。


Den*_*son 8

您很难找出为什么默认设置是“导出所有内容”。自 20 世纪 70 年代诞生以来,该语言及其编译器都发生了巨大的发展,当时互联网上没有发行说明,也没有“工作组”讨论。“结构化编程”和goto语句是当时流行的;很少有人考虑使用封装来最小化共享状态的复杂性问题。Fortran 还使函数公开可见。

我推测,随着该语言越来越流行,出现了更大的系统,这可能会破坏链接器的早期版本。因此需要引入一些规避方法。由于某些疯狂的原因,他们选择使用static隐藏链接器的函数来减少其负载(对我来说,这是一个更大的谜团,而不是为什么链接是任意公开的)。


实际上,在声明函数时static,除了拒绝其他模块访问“内部结构”之外,值得在非常大的程序中隐藏来自链接器的符号,以加快构建时间并减少内存消耗。这很快就会变得难以处理。与其在代码库中散布隐藏static方法,实际上更有意义的是使用编译器选项将隐藏可见性设置为默认值,然后装饰您确实希望对其他模块可见的函数。

在 Linux 中,您可以指示编译器将隐藏可见性设为默认值 ( -fvisibility=hidden):请参阅/sf/answers/3692009471/

事实上,情况比这要复杂一些。还有其他选项可以提供更精细的可见性调整。来自https://gcc.gnu.org/onlinedocs/gcc-4.1.2/gcc/Function-Attributes.html

可见性(“visibility_type”) ELF 目标上的可见性属性导致声明以默认、隐藏、受保护或内部可见性发出。

          void __attribute__ ((visibility ("protected")))
          f () { /* Do something. */; }
          int i __attribute__ ((visibility ("hidden")));
Run Code Online (Sandbox Code Playgroud)

有关完整详细信息,请参阅 ELF gABI,但简短的故事是:

默认

默认可见性是 ELF 的正常情况。该值可用于可见性属性,以覆盖可能更改符号的假定可见性的其他选项。

隐藏可见性表示该符号不会被放入动态符号表中,因此其他模块(可执行文件或共享库)不能直接引用它。

内部的

内部可见性类似于隐藏可见性,但具有附加的处理器特定语义。除非 psABI 另有规定,否则 GCC 定义内部可见性意味着该函数永远不会从其他模块调用。请注意,隐藏符号虽然不能被其他模块直接引用,但可以通过函数指针间接引用。

通过指示不能从模块外部调用符号,GCC 可以例如省略 PIC 寄存器的加载,因为已知调用函数加载了正确的值。

受保护的

受保护的可见性表示符号将被放置在动态符号表中,但定义模块内的引用将绑定到本地符号。也就是说,该符号不能被另一个模块覆盖。

并非所有 ELF 目标都支持此属性。

另请参阅 Peter Cordes 在线程中的评论


另请注意,函数可以被可链接的“螺栓固定”实现覆盖。这对于单元测试中的模拟方法很有用。如果您打算使用“弱链接”属性,那么值得使用它。


值得一提的是,在 C++ 中,首选使用匿名命名空间,而不是static将符号声明为“私有”:

namespace {
    <module-private code>
} // anonymous namespace
Run Code Online (Sandbox Code Playgroud)

请参阅核心指南 SF.22 - https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Rs-unnamed2

根据我的经验,许多公司在其编码标准中都采用了这一点。

请注意,这并不完全等同于“静态”:

静态和匿名命名空间不是一回事。在匿名命名空间中定义的函数将具有外部链接。但它保证存在于唯一命名的范围内。事实上,我们无法在定义它的翻译单元之外引用它,因为它是未命名的。

...因此,对于非常大的 C++ 程序,即使使用匿名命名空间,仍然值得使用和装饰您确实-fvisibility=hidden希望对链接器可见的方法。

  • 这并不能回答问题。 (8认同)
  • 这似乎是一个评论,而不是对问题的回答。 (7认同)
  • “静态”和匿名命名空间不是一回事。在匿名命名空间中定义的函数将具有**外部链接**。但它保证存在于唯一命名的范围内。事实上,我们无法在定义它的翻译单元之外引用它,因为它是未命名的。 (3认同)
  • 如果您不知道,那么您可能不应该发布答案。我不知道,所以我没有发布答案。我不确定是否有人真正知道,研究 C 语言的历史和设计决策从来都不是一件容易的事,除非你遇到一些在事情发生时实际在场的资深人士。 (3认同)
  • ...在这种情况下,这个问题可能应该_没有_答案。 (3认同)
  • `hidden`/`internal` 与 `static` 显着不同;ELF 可见性是指您可能将这些“.o”文件链接到的最终“共享库”之外的可见性。即一个链接器输出。而“static”是关于单个翻译单元(“.c”源文件)的可见性。如果您只是编译可执行文件,而不是共享库,则可见性选项没有任何区别。所以,是的,可见性也是一个跨更大边界的问题(在“.so”共享库之间),但是您的答案写得就像与“static”相同,根本没有区别。 (2认同)

Swi*_*Pie 5

在全局命名空间范围内声明关键字的函数static将具有本地链接。这意味着,那

a) 如果它们在 .cpp 文件中声明,则无法从任何其他编译单元(其他 .cpp 文件)访问它们。

b) 如果它们在头文件中声明,则包含该头文件的每个编译单元中都会有每个函数的副本。

c) 由于它们是在模块中声明的,因此无法从其他任何地方访问它们。

为什么语言要这样设计?对于 C 和 C++ 来说,这都是最初的决定。在 C 中,头文件是次要的、可选的项目。您可以链接其中包含零个头文件的程序。在 C++ 中,您需要在使用之前在源代码中声明函数原型。在 C 中你甚至不需要这个。

C++ 使用相同的策略。你可以说它遵循最小意外原则。这些函数默认具有本地链接并需要“extern”关键字(或“export”,或其他一些扩展的令人厌恶的东西)是出乎意料的。在 C++ 中,匿名命名空间充当与“默认本地链接”最接近的模拟。

  • 同意,但匿名名称空间具有外部链接:) (2认同)
  • @Fareanor,这就是为什么我写了“充当最接近的模拟”。如果没有获取地址,静态 const 变量或函数甚至可能会提前被优化掉,而匿名命名空间不会发生这种情况。 (2认同)
  • 是的,我承认我很挑剔:) (2认同)