4

参考什么决定了两个源文件中的同名类包含哪个类定义?,其中故意,明显违反了一个定义规则,我仍然感到困惑,编译器/链接器甚至可以选择一个定义而不是另一个定义。

(基于答案/评论的附录:我正在寻找一个示例,说明编译器/链接器如何产生如下所示的结果,给出的代码故意违反标准,因此代码导致未定义的行为。)

代码示例是:

// file1.cpp:

#include <iostream>
#include "file2.h"

struct A
{
    A() : a(1) {}
    int a;
};

int main()
{
    // foo() <-- uncomment this line to draw in file2.cpp's use of class A

    A a; // <-- Which version of class A is chosen by the linker?
    std::cout << a.a << std::endl; // <-- Is "1" or "2" output?
}

...

//file2.h:

void foo();

...

// file2.cpp:

#include <iostream>
#include "file2.h"

struct A
{
    A() : a(2) {}
    int a;
};

void foo()
{
    A a; // <-- Which version of class A is chosen by the linker?
    std::cout << a.a << std::endl; // <-- Is "1" or "2" output?
}

在这种情况下,该函数foo()有时会打印 1,有时会打印 2。

但是 for 的构造函数A是内联的!这不是函数调用!因此,我认为编译器必须在编译函数时在函数本身a的编译代码中包含用于实例化对象的代码的汇编/机器指令。foo()foo()

因此,我认为稍后,在链接时,链接器不会更改汇编/机器指令以定义foo()何时决定将函数包含foo()在已编译的二进制文件中(因为它只知道foo()实际上被调用,在链接时)。根据这种推理,链接器不可能影响将哪个内联构造函数代码编译到函数foo()中,因此尽管故意违反了单一定义规则,但始终使用的必须是 file2 的内联构造函数版本。

如果 for 的构造函数A不是内联的,那么我会理解,在foo()编译函数时,函数的 JUMP 语句(构造函数A)可能会放在函数的汇编代码中foo();然后,稍后,在链接时,链接器可以用它选择的构造函数的两个定义来填充 JUMP 语句的地址A

我能想到的唯一解释是,实际上,尽管存在内联构造函数,但有时会foo()打印1,有时会foo()打印2是编译器在编译“file2.cpp”时,会在编译的程序集/机器代码中创建 SPACE 表示用于对 A 的构造函数foo()进行内联调用的函数,但实际上并不填充程序集/机器代码本身;并且稍后,在链接时,链接器将构造函数的代码复制A到函数本身的编译定义中的预定位置foo(),使用它在构造函数的内联函数的两个定义之间的(任意)选择A.

我的解释是正确的,还是有其他解释?在这个例子中,尽管故意违反了单一定义规则,编译器/链接器怎么可能选择调用哪个构造函数A,因为构造函数调用是内联的?

附录:我更改了标题并在顶部附近添加了一段澄清,以回应评论和答案,以明确表示我理解此示例中的行为未定义,并且我正在寻找一个示例来说明如何一个真正的编译器/链接器甚至可以产生一次观察到的行为。请注意,我不是在寻找可以预测任何特定时间行为的答案。

附录 2:作为对评论的回应,我A a;在 VS 调试器的行中放置了一个断点并选择了“反汇编”视图。事实上,从反汇编代码中可以看出,尽管存在“内联”,但在这种情况下,编译器选择不内联对象的构造函数调用a

线`A a; 的反汇编视图  来自问题中的代码示例:编译器选择不内联构造函数调用,尽管它是内联的。

因此,Alf 的回答是正确的:尽管构造函数是隐式inline的,但构造函数调用并没有被内联。

inline因此,一个切题的问题出现了:关于构造函数是否比常规成员函数更不可能被内联(假设在这两种情况下显式或隐式地存在),是否可以做出明确的陈述 - 一种方式或另一种方式?如果可以对此发表声明并且答案是“是的,编译器更有可能拒绝inline构造函数,而不是拒绝inline常规成员函数”,那么后续问题将是“为什么”?

4

3 回答 3

11

在类定义中定义你的构造函数等同于使用关键字inline和它的类外定义。

inline不需要/保证机器代码的内联扩展。它暗示了这一点,但仅此而已。

的保证效果inline是允许在多个翻译单元中定义相同的函数(然后必须在使用它的所有翻译单元中基本上相同地定义它)。

因此,您的逻辑基于对调用的必需/保证内联扩展的假设,由于假设不正确而产生不正确的结论。

于 2012-11-29T12:27:45.100 回答
1

我真的很惊讶代码可能会链接。我几乎可以肯定它没有与 MS Visual Studio 链接,链接器抱怨名称冲突。

在这种情况下,如果你真的不想给它们起不同的名字,你可以使用匿名命名空间来定义你的结构。

于 2012-11-29T12:29:16.417 回答
0

违反 ODR 不需要诊断。也就是说,如果你这样做,你的程序的行为是不确定的:任何事情都可能发生。在示例中,可能不涉及链接器,因为所有成员函数都是内联的,因此代码很可能会毫无怨言地编译和链接,并执行看起来应该执行的操作。然而,该标准并没有保证会发生这种情况。这就是未定义行为的含义。

于 2012-11-29T12:33:50.860 回答