【发布时间】: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 通过多个编译,并在稍后作为整个定义的链接时间组合在一起 -标头工作。