【问题标题】:What determines which class definition is included for identically-named classes in two source files?什么决定了两个源文件中的同名类包含哪个类定义?
【发布时间】:2012-11-14 22:32:35
【问题描述】:

如果我在一个项目中有两个源文件,每个文件都定义了一个同名的类,那么是什么决定了使用哪个版本的类?

例如:

// 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?
}

我已经能够使用相同的代码让链接器选择不同版本的A - 只需更改我键入代码的顺序(沿途构建)。

诚然,在同一个名称空间中包含具有相同名称的类的不同定义是一种糟糕的编程习惯。但是,是否有定义的规则来确定链接器将选择哪个类 - 如果是,它们是什么?

作为这个问题的一个有用的附录,我想知道(一般来说)编译器/链接器如何处理类 - 编译器在构建每个源文件时是否将类名和编译后的类定义合并到对象中文件,而链接器(在名称冲突的情况下)丢弃了一组已编译的类函数/成员定义?

名称冲突的问题并不神秘 - 我现在意识到它每次都会发生一个仅标题的模板文件是 #included 由两个或多个源文件(随后实例化相同的模板类,并且相同在这些多个源文件中调用的成员函数),这是 STL 的常见场景。我认为,每个源文件都必须具有相同实例化模板类函数的单独编译版本,因此链接器必须在链接时在这些函数的不同此类编译版本中进行选择。

-- 关于Java的相关问题的附录--

我注意到各种答案都表明了 C++ 的单一定义规则 (http://en.wikipedia.org/wiki/One_definition_rule)。顺便说一句,我是否正确地认为 Java 没有这样的规则 - 因此 Java 规范允许在 Java 中使用多个不同的定义?

【问题讨论】:

    标签: c++ templates


    【解决方案1】:

    这样的程序违反了单一定义规则并表现出未定义的行为。

    如果程序中有多个类或内联函数定义(在不同的翻译单元或源文件中),则所有定义必须相同。编译器和链接器都不需要诊断所有违反此规则的行为(并非所有违规行为都可以轻松诊断)。

    【讨论】:

      【解决方案2】:

      如果一个 C++ 程序提供了同一个类的两个定义(即,在同一个命名空间中并且命名相同),则该程序违反了标准的规则,您将获得未定义的行为。究竟会发生什么在某种程度上取决于编译器和链接器:有时您会收到链接器错误,但这不是必需的。

      明显的解决方法是不要有冲突的类名。获得唯一类名的最简单方法是在未命名的命名空间中定义本地使用的类型:

      // file1.cpp
      namespace {
          class A { /*...*/ };
      }
      
      // file2.cpp
      namespace {
          class A { /*...*/ };
      }
      

      这两个类不会冲突。

      【讨论】:

      • 标准是否实际上是说不允许一个程序对同一个类提供不同的定义?令我震惊的是,标准不能这么说,因为它不可能强制执行 - 例如,如果两个源文件之一后来被修改并且程序重新构建,但是之前构建的第二个源文件的编译目标文件,与以前版本的编译器,是链接的。因此,也许标准中没有规定禁止不同的类定义?这似乎是一个困难的灰色地带。
      • @DanNissenbaum:没有歧义!该标准明确规定了一个定义规则:3.2 [basic.def.odr] 第 6 段。不过,在评论中引用它太长了。这个定义的重点是编译器和/或链接器可能无法检查是否遵守了约束。因此,该程序具有未定义的行为。
      【解决方案3】:

      这只是成功链接,因为 2 个构造函数的定义隐含为 inline。尝试将它们移到类下,而不是使用 inline 关键字。您正在滥用的链接类型告诉链接器将有多个定义,通常会错误地认为您正在破坏一个定义规则,而您实际上正在破坏该规则。通常,这种允许您看似破坏 ODR 的条件存在于模板之类的东西中,模板在不同的翻译单元中总是有多个相同的定义。但这就是条件:不同翻译单元中的定义必须相同。

      在您的示例中,最终取决于您的编译器。

      【讨论】:

        【解决方案4】:

        如果您允许(您应该这样做),编译器会针对多个定义向您发出警告。

        gnu 链接器按照您在命令行上显示文件的顺序解析符号,因此它使用它看到的第一个定义。不确定是否所有链接器都以相同的方式工作。

        【讨论】:

          【解决方案5】:

          One Definition Rule 存在的原因是无论使用哪个定义都无关紧要,它们都是相同的。至于使用哪个版本,或者它们是否一致,完全取决于所讨论的编译器和链接器。唯一可以从外部看到的副作用是当函数内部存在静态变量时,必须在函数的所有实例之间使用该变量的单个实例。

          通过违反单一定义规则,您以一种与正确编写的程序无关的方式暴露了编译器/链接器的机制。

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 2010-10-30
            • 2016-11-12
            • 1970-01-01
            • 2011-03-27
            • 2015-07-21
            • 2015-12-10
            • 1970-01-01
            相关资源
            最近更新 更多