链接器如何处理 C++ 中 headers 中的定义?

How does the linker deal with definitions in headers in C++?

我想更好地了解链接器在构建 C++ 代码时是如何工作的。

如果我在多个 cpp 文件中定义函数或全局变量,我会收到多个定义的链接器错误。这是有道理的,因为我有多个版本,链接器无法决定特定版本。为了避免这种情况,只有一个 writes/includes 声明(签名仅用于函数,extern 用于变量)。 但是,我注意到您可以在 class 声明中定义方法,并且至少这里的大多数人认为对于琐碎的函数(如琐碎的 getter 和 setter)来说,这是可以接受的甚至是好的做法,因为它允许编译器内联这些函数(而且,模板也是必需的)。

在"pragma once"的讨论中,我了解到在某些情况下,工具链将无法区分文件是否相同,因此原则上可能会发生两个cpp文件获得从不同 headers 声明的相同 class 名称,但对此类 header-only 方法的定义不同,不是吗?

我试过举个例子: main.cpp

#include <iostream>
#include "Class1.hpp"
#include "Class2.hpp"

using namespace std;

int main() {
  Class1 c1;
  Class2 c2(c1);

  c1.set(1);
  cout << c1.get() << endl;
  c2.print();

  return 0;
}

Class1.hpp:

#ifndef CLASS1_HPP
#define CLASS1_HPP
#warning Class1

class Class1 {
  public:
  void set(int i) { val = i; };
  int get() {return val;};

  int val=0;
};

#endif

Class1a.hpp

#ifndef CLASS1_HPP
#define CLASS1_HPP
#warning Class1a

class Class1 {
  public:
  void set(int i) { val = i; };
  int get() {return -1*val;};

  int val=0;
};

#endif

Class2.hpp:

#pragma once
#ifndef CLASS2_HPP
#define CLASS2_HPP

#include <iostream>
#include "Class1a.hpp"

using namespace std;

class Class2 {
  public:
  Class2(Class1 &c1) : c1(c1) {};
  void print();

  Class1& c1;
};

#endif

Class2.cpp

#include "Class2.hpp"

void Class2::print() {
  cout << c1.get() << endl;
}

但是,我得到以下输出:

$ g++ *.cpp; ./a.out     
In file included from Class2.hpp:6:0,
                 from Class2.cpp:1:
Class1a.hpp:4:2: warning: #warning Class1a [-Wcpp]
 #warning Class1a
  ^~~~~~~
-1
-1

我不太明白为什么 Class1(not-a) 从未被预编译器看到,尽管它首先包含在 main.cpp 中,所以我想我的问题延伸到那个。 .. [编辑:我无法再重现预编译器问题,这现在产生与下面代码相同的结果,正如我最初预期的那样]

编辑:删除 pragma 一次以避免进一步的混淆和偏差。


好吧,因为人们似乎把它弄混了,这是我预期预编译器的结果:

main.cpp:

#include <iostream>
using namespace std;

class Class1 {
  public:
  void set(int i) { val = i; };
  int get() {return val;}; // <-- This line is different!

  int val=0;
};

class Class2 {
  public:
  Class2(Class1 &c1) : c1(c1) {};
  void print();

  Class1& c1;
};

int main() {
  Class1 c1;
  Class2 c2(c1);

  c1.set(1);
  cout << c1.get() << endl;
  c2.print();

  return 0;
}

Class2.cpp:

#include <iostream>
using namespace std;

class Class1 {
  public:
  void set(int i) { val = i; };
  int get() {return -1*val;};

  int val=0;
};


class Class2 {
  public:
  Class2(Class1 &c1) : c1(c1) {};
  void print();

  Class1& c1;
};

void Class2::print() {
  cout << c1.get() << endl;
}

不知道为什么之前的预编译器不起作用。也许有人愿意解释,尽管这不是我的主要问题。而且,是的,我当然知道编写这样的代码是个坏主意,我只是想知道它是如何处理的。完全学术问题。

我现在发现可执行文件的输出取决于我声明 g++ 的 cpp 文件的顺序:

$ g++ main.cpp Class2.cpp
$ ./a.out                
1
1
$ g++ Class2.cpp main.cpp 
$ ./a.out                 
-1
-1

所以在某些时候,链接器似乎获取了该方法的下一个最佳版本。为什么函数和变量似乎不会发生同样的情况,并且可以避免这种情况(因为这似乎至少应该产生警告)?


附加功能示例。 main.cpp

#include <iostream>
using namespace std;

int get() {return 1;} 
void print();

int main() {
  cout << get() << endl;
  print();
}

method2.cpp

int get() { return -1; }

void print() {
  cout << get() << endl;
}

在这里,多重定义被捕获:

$ g++ main.cpp method2.cpp 
/tmp/ccjCKBLm.o: In function `get()':
method2.cpp:(.text+0x0): multiple definition of `get()'
/tmp/ccnvH0iR.o:main.cpp:(.text+0x0): first defined here
/tmp/ccnvH0iR.o: In function `main':
main.cpp:(.text+0x38): undefined reference to `print()'
collect2: error: ld returned 1 exit status

如果我将内联添加到函数中,那么它会再次编译,但总是 returns 1,尽管 g++ 的参数顺序与下面的获胜答案一致(没有双关语意)。

链接器根本不处理 header 文件。编译器(在预处理器阶段)以文本方式将 headers 的内容插入到源文件中。然后编译生成的翻译单元。

pragma once 与您的示例无关;你使用 equivalent-but-portable header 守卫。同样,这是预处理器的事情,所以 header 守卫也不会被链接器看到。它们防止在单个翻译单元中重复插入单个 header。

至于实际的 C++,您违反了单一定义规则(正如 Max Langhof 在评论中指出的那样)。这意味着所有投注都是;这是未定义的行为。存在未定义行为时没有 "understanding how the linker works"。

查看 [basic.def.odr]/10,我们有:

Every program shall contain exactly one definition of every non-inline function or variable that is odr-used in that program outside of a discarded statement; no diagnostic required. [...]

这使得 (non-inline) 函数或变量的多个定义成为 ODR 违规。 不需要链接器来对此进行诊断,但由于这样做通常很容易,因此您会经常看到此诊断。

然后我们有[basic.def.odr]/12:

There can be more than one definition of a

[...]

  • inline function [...]

[...]

in a program provided that each definition appears in a different translation unit, and provided the definitions satisfy the following requirements. [...] Given such an entity named D defined in more than one translation unit, then

  • each definition of D shall consist of the same sequence of tokens, [...]

[...]

[...] If the definitions of D do not satisfy these requirements, then the program is ill-formed, no diagnostic required.

您的 Class1::get 方法违反了这一点。它是隐含的 inline(因为它是在 class 定义中定义的,请参阅 [dcl.inline]/4 - 上述规则也在该部分中进行了总结),因此允许有多个定义,但它们确实如此不包含相同的标记序列。

再次强调,不需要诊断。检查内联函数的多个定义的一致性(以及我在上面引号中跳过的所有其他内容)对于链接器来说是不可行的,因此它通常不会尝试这样做。


in principle, it could happen that two cpp files get the same class name declared from different headers, but with different definitions of such header-only methods, couldn't it?

这可能会发生,是的,这将违反 ODR,使程序 ill-formed 无需诊断。使用include guard宏是一个可靠的对策。

请注意,这不仅限于不同的 header 声明相同的 class,还包括例如相同的 header 包含在不同的 #define 情况下,因此同一 header 文件的包含之间的预处理定义不同。

How does the linker deal with definitions in headers in C++?

如果跨翻译单元有多个内联定义,则链接器会选择其中一个定义并丢弃其余定义。只需要一个,因为所有定义必须相同。

[pragma once] it could happen that two cpp files get the same class name declared from different headers, but with different definitions of such header-only methods, couldn't it?

这不可能是错误识别 pragma once 的结果。 header 的内容仍然相同,因此函数的定义是相同的。这种情况的问题是在单个翻译单元内会有类型、non-inline 函数或变量的多个定义,这也违反了一个定义规则。幸运的是,这种类型的违规对于编译器诊断来说是微不足道的。

I don't quite get why Class1(not-a) is never seen by the precompiler

被pre-compiler看到了。从输出可以看出:

In file included from main.cpp:2:
./Class1.hpp:3:2: warning: Class1 [-W#warnings]
#warning Class1
 ^
1 warning generated.
In file included from Class2.cpp:1:
In file included from ./Class2.hpp:6:
./Class1a.hpp:3:2: warning: Class1a [-W#warnings]
#warning Class1a

No idea why that did not work.

它没有用,因为你违反了 ODR。因此你的程序是ill-formed。诊断此特定问题不需要实现(即工具链,即编译器、链接器等)。如果需要链接器来诊断问题,它就必须检查每个翻译单元中的每个内联定义,以确保它们是相同的。这对于大型编译来说可能会变得相当昂贵。

Why does the same not seem to happen with functions and variables

同样适用于所有内联函数和内联变量(内联变量是 C++17 中的新事物),而不仅仅是内联成员函数。 non-inline 函数或 non-inline 变量不是问题,因为 ODR 规则只允许所有翻译单元有一个定义,所以当链接器找到多个定义时,它可以很容易地告诉你搞砸了。

could it be avoided (because this seems like something that should at least produce a warning)?

我还没有看到任何可以诊断此违规行为的链接器。我的最佳建议是制定良好的命名规则(使用命名空间以避免名称冲突)和测试规则(以便在发生冲突时检测到错误行为)。