C ++:不同翻译单元中具有相同名称的不同类

时间:2012-02-20 16:35:23

标签: c++ one-definition-rule

考虑以下示例:

// 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 hit
      UsedClass 1(0x7fffcef79fff)doit hit

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

      

    UsedClass 1(0012FF5B)doit hit
      UsedClass 1(0012FF67)doit hit

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

      

    UsedClass 1(0012FF73)doit hit
      UsedClass 1(0012FF7F)doit hit

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

  1. 为什么输出[EG1],[EG3],[EG4]可能?
  2. 为什么我从不同的编译器甚至是同一个编译器得到不同的结果?这让我觉得标准在某种程度上没有指明这种情况下的行为。
  3. 感谢您提出任何建议,意见和标准解释。

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

3 个答案:

答案 0 :(得分:13)

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

  

类类型的定义可以有多个(第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对象{1}},并使用常量表达式(5.19)初始化对象,并使用对象的值(但不是地址),并且对象在D的所有定义中具有相同的值;和

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

  •   
  • D的每个定义中,所引用的重载运算符,对转换函数,构造函数,运算符新函数和运算符删除函数的隐式调用,应引用相同的函数或函数在D的定义中定义;以及

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

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

  •   

在您的程序中,您违反了D的ODR,因为令牌在不同的编译单元中有所不同。你可以通过在类体外移动class UsedClass的定义来解决这个问题,但同样的规则也适用于内联函数体。

答案 1 :(得分:7)

您的程序违反了One Definition Rule并调用了未定义的行为 如果您中断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的每个定义应由相同的令牌序列组成; 和   ...

答案 2 :(得分:4)

  

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

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

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

  

这让我觉得标准在某种程度上没有指明这种情况下的行为。

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