【问题标题】:When should linkers generate multiply defined X warnings?链接器何时应生成多重定义的 X 警告?
【发布时间】:2010-11-28 23:40:20
【问题描述】:

永远不要拒绝 C++。会搞砸的。

我习惯于为我所做的一切编写单元测试。作为其中的一部分,我经常在测试的 .cxx 中定义名称为 A 和 B 的类来练习代码,安全地知道 i) 因为此代码永远不会成为库的一部分或在测试之外使用,名称冲突可能非常频繁,并且 ii) 可能发生的最坏情况是链接器会抱怨多重定义的 A::A() 或什么,我将修复该错误。我错了。

这里有两个编译单元:

#include <iostream>
using namespace std;

// Fwd decl.
void runSecondUnit();

class A {
public:
   A() : version( 1 ) {
      cerr << this << "   A::A()  --- 1\n";
   }    
   virtual ~A()   {
      cerr << this << "   A::~A()  --- 1\n";
   }

   int version;    };

void runFirstUnit()  {
   A a;
   // Reports 1, correctly.
   cerr << "   a.version = " << a.version << endl;
   // If you uncomment these, you will call
   // secondCompileUnit: A::getName() instead of A::~A !
   //A* a2 = new A;
   //delete a2;
}

int main( int argc, char** argv )  {
   cerr << "firstUnit BEGIN\n";
   runFirstUnit();
   cerr << "firstUnit END\n";

   cerr << "secondUnit BEGIN\n";
   runSecondUnit();
   cerr << "secondUnit END\n";
}

#include <iostream>
using namespace std;

void runSecondUnit();

// Uncomment to fix all the errors:
//#define USE_NAMESPACE
#if defined( USE_NAMESPACE )
   namespace mySpace
   {
#endif

class A  {
   public:
   A() :  version( 2 )  {
      cerr << this << "   A::A()  --- 2\n";
   }

   virtual const char* getName() const {
      cerr << this << "   A::getName()  --- 2\n"; return "A";
   }

   virtual ~A()  {
      cerr << this << "   A::~A()  --- 2\n";
   }

   int version;
};


#if defined(USE_NAMESPACE )
   } // mySpace
   using namespace mySpace;
#endif

void runSecondUnit() {   
   A a;   
   // Reports 1. Not 2 as above!
   cerr << "   a.version = " << a.version << endl;
   cerr << "   a.getName()=='" << a.getName() << "'\n";    
}

好的,好的。显然,我不应该声明两个名为 A 的类。我的错。但我敢打赌,你猜不到接下来会发生什么……

我编译了每个单元,并链接了两个目标文件(成功)并运行了。嗯……

这是输出 (g++ 4.3.3):

firstUnit BEGIN
0x7fff0a318300   A::A()  --- 1
   a.version = 1
0x7fff0a318300   A::~A()  --- 1
firstUnit END
secondUnit BEGIN
0x7fff0a318300   A::A()  --- 1
   a.version = 1
0x7fff0a318300   A::getName()  --- 2
   a.getName()=='A'
0x7fff0a318300   A::~A()  --- 1
secondUnit END

所以有两个单独的 A 类。在第二次使用中,使用了第一个 on 的析构函数和构造函数,即使只有第二个在其编译单元中可见。更奇怪的是,如果我取消注释 runFirstUnit 中的行,而不是调用 A::~A,而是调用 A::getName。显然,在第一次使用中,对象获取了第二个定义的 vtable(getName 是第二个类中的第二个虚函数,析构函数是第一个类中的第二个)。它甚至从一开始就正确地获取了构造函数。

所以我的问题是,为什么链接器不抱怨多重定义的符号。 它似乎选择了第一个匹配项。重新排序链接步骤中的对象确认。

Visual Studio 中的行为是相同的,所以我猜这是一些标准定义的行为。我的问题是,为什么?显然,链接器很容易在给定重复名称的情况下出错。 如果我添加,

 void f() {}

它抱怨的两个文件。为什么不使用我的类构造函数和析构函数?

编辑问题不在于“我应该怎么做才能避免这种情况”,或者“如何解释这种行为”。它是,“为什么链接器不捕捉它?”项目可能有数千个编译单元。明智的命名实践并不能真正解决这个问题——它们只会让问题变得模糊,而且只有在你可以训练每个人都遵循它们的情况下。

上面的示例导致了模棱两可的行为,编译器工具可以轻松且明确地解决这些问题。那么,他们为什么不呢?这只是一个错误。 (我怀疑不是。)

** 编辑 ** 请参阅下面的 litb 答案。我重复一遍以确保我的理解是正确的:

链接器只为强引用生成警告。 因为我们有共享头文件,所以内联函数定义(即在同一个地方进行声明和定义的地方,或模板函数)被编译到每个看到它们的 TU 的多个目标文件中。因为没有简单的方法可以将此代码的生成限制为单个目标文件,所以链接器需要从多个定义中选择一个。因此链接器不会生成错误,这些编译定义的符号在目标文件中被标记为弱引用。

【问题讨论】:

  • 通常,单元测试支持者会说您的测试应该用作文档。如果您将测试类命名为 A 和 B,我不确定您是如何实现的。出于某种原因,允许使用多字符名称。为你的类命名,即使它们是用于单元测试的。
  • 嗯,当然......但这并不能解决任何问题。项目有数百万行代码,都在单独的编译单元中。不能保证有人不会重新声明具有相同名称的类型尤其是,因为他们是根据目的来命名的。这就是链接器的工作(或可能是链接器的工作)。此外,上面的类名仅供您使用。我之所以遇到这个,是因为我在实践中在两个地方使用了相同的合理名称。
  • 链接器只考虑标识符,而不考虑它们的代码。从链接器的 POV 来看,您的情况与 A 是同一类、在标头中定义并包含在两个 cpp 文件中的情况有什么区别?
  • sbi:当然,没有符号(尽管您可能有理由希望链接器在多个定义的目标代码不同时产生错误——他们不检查这个)。您可能会问在链接器确实会产生错误的多个文件中具有重复定义的函数的相同问题——那么为什么在这种情况下会出现错误而不是其他情况呢?我认为 litb 的答案是正确的 - inline 函数 must 由单独的 TU 通过多个编译,并在稍后作为整个定义的链接时间组合在一起 -标头工作。

标签: c++ linker


【解决方案1】:

函数被定义为内联。内联函数可以在程序中定义多次。请参阅此处摘要中的第 3 点:

http://en.wikipedia.org/wiki/One_Definition_Rule

重点是:

对于给定的实体,每个定义必须相同。

尽量不要将函数定义为内联。然后链接器应该开始给出重复的符号错误。

【讨论】:

  • 这些函数被证明没有内联。为什么?因为每个编译单元中的代码之间都有交互。第一个单元可以调用第二个析构函数。第二个单元调用第一个的构造函数。只有链接器会导致这种行为。
  • 还有。这个问题很容易通过多种方式解决。这个问题是为什么它是一个问题?
  • 函数是否实际内联无关紧要;如果它们是内联定义(或声明)的,则它们算作内联,并且允许多个定义。这就是为什么没有生成错误消息的原因:在这种情况下有多个定义不是错误。
【解决方案2】:

编译器和链接器依赖于两个类完全相同。在你的情况下,它们是不同的,所以会发生奇怪的事情。一个定义规则说结果是未定义的行为 - 因此行为根本不需要在编译器之间保持一致。 .我怀疑在runFirstUnit 的删除行中,它调用了第一个虚拟表条目(因为在 its 翻译单元中,析构函数可能占据了第一个条目)。

在第二个翻译单元中,该条目恰好指向A::getName,但在第一个翻译单元(执行delete)中,该条目指向A::~A。由于这两个名称不同(A::~AA::getName),因此不会出现名称冲突(您将为析构函数和 getName 发出代码)。但是由于它们的类名相同,它们的 v-tables 故意冲突,因为由于两个类具有相同的名称,链接器会认为它们 是同一个类并假设相同的内容。

请注意,所有成员函数都是在类中定义的,这意味着它们都是内联函数。这些函数可以在一个程序中定义多次。在类内定义的情况下,基本原理是您可以将相同的类定义从其头文件包含到不同的翻译单元中。但是,您的测试函数不是内联函数,因此将其包含到不同的翻译单元中会触发链接器错误。

如果启用命名空间,就不会发生什么冲突,因为::A::mySpace::A 是不同的类,当然会得到不同的 v-tables。

【讨论】:

  • 但是构造函数同名。为什么他们没有名字冲突?似乎完全有可能并且可以避免错误。此外,如果两个文件使用相同的命名空间,添加命名空间(匿名命名空间除外)并不能保证避免名称冲突。
  • 也会有名字冲突。但是由于构造函数是内联的,链接器会假设它们都是以相同的方式定义的,并且只会将它们的一个定义发送到二进制文件中(内联函数的地址在翻译单元之间必须相同),丢弃其他定义。尝试在类定义之外定义它们,你也会得到链接器错误,很可能
  • 有点像模板实例化。如果你交换(a,b); a 和 b 都是来自两个翻译单元的整数,您也不会期望出现链接器错误,尽管编译器可能两次都生成了一个函数来进行交换。神奇的是,链接器将合并所有实例化,因此它们最终仅作为一个函数实例。在实践中,据我所知,它通常会丢弃除其中一个之外的所有内容。
  • 在您的测试中,第一个翻译单元没有使用相同的命名空间。所以在另一个中使用mySpace::A,当然可以有效地解决问题。
  • 构造函数真的是内联的吗?我不这么认为。为什么?因为 object 接收到另一个的 vtable 指针。构造函数没有设置 vtable 指针吗?如果没有来自其他翻译单元的代码,这怎么可能?嗯……让我试试……
【解决方案3】:

将每个类限制为当前翻译单元的一种简单方法是将其包含在匿名命名空间中:

// a.cpp
namespace {
  class A {
    // ...
  };
}

// b.cpp
namespace {
  class A {
    // ...
  };
}

完全合法。因为这两个类在不同的翻译单元中,并且在匿名命名空间中,所以它们不会冲突。

【讨论】:

  • 对于我的回归测试来说,这是一个更好的模式。谢谢。但是问题是什么——为什么链接器不产生错误?
猜你喜欢
  • 2012-12-09
  • 1970-01-01
  • 1970-01-01
  • 2010-11-28
  • 2013-04-05
  • 1970-01-01
  • 1970-01-01
  • 2013-07-19
相关资源
最近更新 更多