【问题标题】:Understanding what causes this multiple definition error了解导致此多重定义错误的原因
【发布时间】:2017-01-09 00:31:21
【问题描述】:

我有一个由两个类实现的纯虚方法的基类:

// base_class.hpp
class base_class {
public:
  virtual std::string hello() = 0;
};

// base_implementer_1.hpp
class base_implementer1 : base_class {
public:
  std::string hello();
};

// base_implementer_2.hpp
class base_implementer2 : base_class {
public:
  std::string hello();
};

// base_implementer_1.cpp
std::string hello() {
  return(std::string("Hello!"));
}

// base_implementer_2.cpp
std::string hello() {
  return(std::string("Hola!"));
}

请注意在实现中缺少base_implementer1:: 和base_implementer2::。这是故意的。

通过添加base_implementer1:: 和base_implementer2::,我没有收到多重定义错误。但是,将它们从链接器中删除会抱怨我对同一函数有两个定义 (hello())。

由于头文件中没有这两个实现,我认为(即使它们在实际实现 hello() 方面是不正确的)它们是被允许的,因为没有理由你不能有两个 @ 987654328@ 在两个不同的.cpp 文件中起作用。但情况似乎并非如此。谁能告诉我链接器中发生了什么导致这个多重定义错误发生?

【问题讨论】:

  • “因为没有理由在两个不同的 .cpp 文件中不能有两个 hello() 函数”——是的,有。链接器看到两个具有相同签名的函数 - 这是一个多重定义。
  • 您可以在两个不同的 .cpp 文件中拥有两个 hello(),但不能将它们链接在一起。否则,当调用hello() 时(例如在main.cpp 中)应该使用哪个定义?
  • 仅仅因为您的hello 实现在base_implementer_1.cpp 中并不意味着它是base_implementer_1 类的一部分。只有当您用base_implementer_1:: 假装hello 时,它才会成为课程的一部分。 base_implementer_2 也是如此。换句话说,你故意没有在方法实现前加上类名是你的问题。
  • 请发布 MCVE。当前代码将无法编译,因为 std 未定义。
  • @M.M 这是一个说明概念的玩具示例。它的编译能力并不能增强对手头问题的理解。我们可以想象这些文件被声明在一个包含必要的#includes(等)的地方

标签: c++ linker one-definition-rule


【解决方案1】:

One-Definition-Rule 定义了两个范围的规则,即翻译单元范围和程序范围。

以下具有翻译单元范围的规则规定,同一翻译单元不得包含同一功能的两个不同定义:

任何变量、函数、类类型、枚举只有一个定义 任何一个翻译单元中都允许使用类型或模板(某些 这些可能有多个声明,但只有一个定义是 允许)。

因此,如果您有两个不同的 .cpp 文件,那么您就有两个不同的翻译单元,并且每个翻译单元都可能有自己的 hello() 定义;在翻译单元的范围内不违反 ODR。

以下具有程序范围的规则定义了一个 odr-used 函数必须在程序中只定义一次:

每个非内联函数或变量的一个且只有一个定义 odr-used(见下文)必须出现在整个 程序(包括任何标准和用户定义的库)。这 编译器不需要诊断这种违规行为,但行为 违反它的程序是未定义的。

odr-used 的定义非正式地指出,对于每个被调用的函数或获取的地址都必须在程序中定义:

非正式地,如果一个对象的地址被占用,或者一个对象是 odr-used 引用绑定到它,如果一个函数,一个 函数是 odr-used 调用它或获取它的地址。如果一个物体或一个 函数是 odr 使用的,它的定义必须存在于 程序;违反此规定是链接时错误。

因此,如果多个 .cpp 文件公开了 hello() 的实现,并且如果调用或引用了此函数,则显然违反了程序范围的 ODR。

如果相应的功能没有被 ODR 使用(即被调用或引用),那么 ODR 应该——据我的理解——不被违反;

如果编译器抱怨重复符号,那是因为程序违反了链接规则(也请就“如果我不使用 odr-use 变量”提供SO answer)。 C++11 §3.5[basic.link]/9 状态:

两个相同的名字在不同的作用域中声明 应表示相同的变量、函数、类型、枚举器、模板 或命名空间如果

  • 两个名称都有外部链接,否则两个名称都有内部链接并在同一个翻译单元中声明;和...

为避免这种情况,请确保最多公开一个 hello() 的实现,并让所有其他实现 static 或使用未命名的命名空间。

在 C 编程语言中,static 与全局变量和函数一起使用,以将其范围设置为包含文件,即它不公开此实现并且避免与其他二进制文件的名称冲突。

因此,一个合理的建议是:使仅在翻译单元内使用的函数定义仅对该翻译单元可见;并定义在命名空间或类中公开的函数,以避免链接器中出现意外或不可预见的名称冲突/重复符号问题。

【讨论】:

  • 这是最完整的答案。谢谢。
  • OP 确实违反了 ODR。您的报价不是 ODR 的完整定义。请参阅链接页面中引用的段落之后的下一段。
  • @user2079303 从技术上讲,它仅在 hello 是 odr-used 时才违反 ODR,它不在 OP 的代码中。 See this thread。如果这个答案能更具体一点就更好了
  • OP 可以根据@M.M 发布更完整的答案吗?如果 Stephan 这样做,我会留下这个选定的答案。
  • @M.M:感谢您的意见! AT_rec:修改了答案;希望论证是一致和完整的。
【解决方案2】:

您在两个不同的翻译单元中对名为hello 的函数有两个不同的定义。在链接时间方面,链接器不知道要链接到哪个hello 函数。

考虑:

A.cpp

#include <string>
std::string hello() {
    return "A";
}

B.cpp

#include <string>
std::string hello() {
    return "B";
}

C.cpp

#include <iostream>
std::string hello();
int main() {
    std::cout << hello() << '\n';
}

链接器怎么可能知道在main 中调用哪个hello?不能,因为违反了单一定义规则。

【讨论】:

    【解决方案3】:

    您在 base_implementor_1.cpp 中定义了一个名为 hello 的全局函数。您在 base_implementor_2.cpp 中定义了另一个名为 hello 的全局函数。这会导致多重定义和违反 ODR 所需的错误。为什么这是个问题?如果您有第三个源文件调用hello(),应该调用哪个函数?

    如果您想在多个源文件中定义具有相同名称的不同函数,可以在它们前面加上 static 关键字

    static void hello() { }
    

    或在匿名命名空间内

    namespace {
        void hello() { }
    }
    

    【讨论】:

    • 允许多个函数声明。问题出在多个定义上。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-06-07
    相关资源
    最近更新 更多