8

考虑以下示例:

// usedclass1.hpp  
#include <iostream>  
class UsedClass
{  
public:
  UsedClass() { }  
  void doit() { std::cout << "UsedClass 1 (" << this << ") doit hit" << std::endl; }
};  

// usedclass2.hpp  
#include <iostream>
class UsedClass
{
public:
  UsedClass() { }
  void doit() { std::cout << "UsedClass 2 (" << this << ") doit hit" << std::endl; }
};

// object.hpp
class Object
{
public:
  Object();
};

// object.cpp
#include "object.hpp"
#include "usedclass2.hpp"
Object::Object()
{
  UsedClass b;
  b.doit();
}

// main.cpp
#include "usedclass1.hpp"
#include "object.hpp"
int main()
{
  Object obj;
  UsedClass a;
  a.doit();
}

代码编译时没有任何编译器或链接器错误。但输出对我来说很奇怪:

  • Fedora x86_64 上的 gcc (Red Hat 4.6.1-9) 没有优化 [ EG1 ]:

    UsedClass 1 (0x7fff0be4a6ff) doit hit
    UsedClass 1 (0x7fff0be4a72e) doit hit

  • 与 [EG1] 相同,但启用了 -O2 选项 [ EG2 ]:

    UsedClass 2 (0x7fffcef79fcf) doit 命中
    UsedClass 1 (0x7fffcef79fff) doit 命中

  • Windows XP 32bit 上的 msvc2005 (14.00.50727.762) 没有优化 [ EG3 ]:

    UsedClass 1 (0012FF5B) doit 命中
    UsedClass 1 (0012FF67) doit 命中

  • 与 [EG3] 相同,但启用了 /O2(或 /Ox)[ EG4 ]:

    UsedClass 1 (0012FF73) doit 命中
    UsedClass 1 (0012FF7F) doit 命中

我期望链接器错误(假设违反了 ODR 规则)或 [EG2] 中的输出(内联代码,没有从翻译单元导出任何内容,保留 ODR 规则)。因此我的问题:

  1. 为什么输出 [EG1]、[EG3]、[EG4] 可能?
  2. 为什么我从不同的编译器甚至从同一个编译器得到不同的结果?这让我认为标准在这种情况下没有指定行为。

感谢您的任何建议、意见和标准解释。

更新
我想了解编译器的行为。更准确地说,如果违反了 ODR,为什么不会产生错误。一个假设是,由于UsedClass1UsedClass2类中的所有函数都被标记为内联(因此违反 C++03 3.2),因此链接器不会报告错误,但在这种情况下会输出 [EG1]、[EG3]、[ EG4] 看起来很奇怪。

4

3 回答 3

14

这是标准第 3.2 节中禁止您正在做的事情的规则(C++11 措辞):

类类型(第 9 条)、枚举类型(7.2)、带外部链接的内联函数(7.1.2)、类模板(第 14 条)、非静态函数模板(14.5.6)可以有多个定义、类模板的静态数据成员 (14.5.1.3)、类模板的成员函数 (14.5.1.1) 或在程序中未指定某些模板参数的模板特化 (14.7, 14.5.5) ,前提是每个定义出现在不同的翻译单元中,并且定义满足以下要求。给定这样一个D在多个翻译单元中定义的实体,那么

  • 每个定义D应由相同的标记序列组成;和

  • 在 的每个定义中D,根据 3.4 查找的对应名称应指在 的定义中定义的实体D,或应指同一实体,在重载决议 (13.3) 和部分模板特化 (14.8.3) 匹配之后),但const如果对象在 的所有定义中具有相同的文字类型D,并且该对象使用常量表达式 (5.19) 初始化,并且值(但不是地址) 被使用,并且该对象在 的所有定义中具有相同的值D;和

  • 在 的每个定义中D,对应的实体应具有相同的语言链接;和

  • 在 的每个定义中D,所指的重载操作符、对转换函数、构造函数、操作符新函数和操作符删除函数的隐式调用,应指同一函数,或 D 定义中定义的函数;和

  • 在 的每个定义中D,(隐式或显式)函数调用使用的默认参数被视为其标记序列存在于 的定义中D;也就是说,默认参数受上述三个要求的约束(并且,如果默认参数具有带有默认参数的子表达式,则此要求递归适用)。

  • 如果D是一个具有隐式声明的构造函数(12.1)的类,就好像构造函数是在使用它的每个翻译单元中隐式定义的,并且每个翻译单元中的隐式定义应为基类调用相同的构造函数的类或类成员D

在您的程序中,您违反了 ODR,class UsedClass因为令牌在不同的编译单元中是不同的。您可以通过将定义移到UsedClass::doit()类主体之外来解决这个问题,但同样的规则也适用于内联函数的主体。

于 2012-02-20T16:51:14.200 回答
7

您的程序违反了单一定义规则并调用了未定义的行为。
如果您破坏 ODR 但行为未定义,则该标准不强制要求提供诊断消息。

C++03 3.2 一定义规则

任何翻译单元不得包含一个以上的任何变量、函数、类类型、枚举类型或模板的定义。...

每个程序都应包含该程序中使用的每个非内联函数或对象的一个​​定义;无需诊断。定义可以显式出现在程序中,可以在标准或用户定义库中找到,或者(在适当时)隐式定义(参见 12.1、12.4 和 12.8)。内联函数应在使用它的每个翻译单元中定义。

此外,该标准定义了存在多个符号定义的特定要求,这些在 3.2 的第 5 段中得到了恰当的定义。

类类型(第 9 条)、枚举类型(7.2)、带有外部链接的内联函数(7.1.2)、类模板(第 14 条)、非静态函数模板(14.5.5)可以有多个定义、类模板的静态数据成员 (14.5.1.3)、类模板的成员函数 (14.5.1.1) 或在程序中未指定某些模板参数的模板特化 (14.7, 14.5.4),前提是每个定义出现在不同的翻译单元中,并且定义满足以下要求。给定这样一个名为 D 的实体在多个翻译单元中定义,则

D 的每个定义应由相同的标记序列组成;和 ...

于 2012-02-20T16:40:10.790 回答
4

为什么输出 [EG1]、[EG3]、[EG4] 可能?

简单的答案是行为是未定义的,所以一切皆有可能。

大多数编译器通过在定义它的每个翻译单元中生成一个副本来处理内联函数。然后链接器任意选择一个包含在最终程序中。这就是为什么在禁用优化的情况下,它在两种情况下都调用相同的函数。启用优化后,编译器可能会内联函数,在这种情况下,每个内联调用都将使用当前翻译单元中定义的版本。

这让我认为标准在这种情况下没有指定行为。

这是正确的。打破一个定义规则会产生未定义的行为,并且不需要诊断。

于 2012-02-20T16:45:29.833 回答