为什么静态成员的类内初始化违反了ODR?

时间:2013-09-20 05:42:54

标签: c++ static-members language-lawyer one-definition-rule in-class-initialization

Stack Overflow上有几个问题:“为什么我不能在C ++中初始化类中的静态数据成员”。大多数答案引用标准,告诉你你可以做什么;那些试图回答为什么通常指向一个链接(现在似乎不可用)[编辑:实际上它可用,见下文]在Stroustrup的网站上,他声明允许静态成员的类内初始化违反一个定义规则(ODR)。

但是,这些答案似乎过于简单化了。编译器完全能够在需要时解决ODR问题。例如,请考虑C ++标题中的以下内容:

struct SimpleExample
{
    static const std::string str;
};

// This must appear in exactly one TU, not a header, or else violate the ODR
// const std::string SimpleExample::str = "String 1";

template <int I>
struct TemplateExample
{
    static const std::string str;
};

// But this is fine in a header
template <int I>
const std::string TemplateExample<I>::str = "String 2";

如果我在多个翻译单元中实例化TemplateExample<0>,编译器/链接器魔法就会启动,我在最终的可执行文件中只得到TemplateExample<0>::str的一个副本。

所以我的问题是,鉴于编译器显然可以解决模板类的静态成员的ODR问题,为什么它也不能为非模板类​​执行此操作呢?

编辑:Stroustrup常见问题解答响应可用here。相关的句子是:

  

但是,为避免复杂的链接器规则,C ++要求每个对象都有唯一的定义。如果C ++允许将需要作为对象存储在内存中的实体的类内定义

,则该规则将被破坏

然而,似乎那些“复杂的链接器规则”确实存在并且在模板案例中使用,那么为什么不在简单的情况下呢?

2 个答案:

答案 0 :(得分:1)

C ++ Build结构过去非常简单。

编译器构建了通常包含一个类实现的目标文件。 然后,链接器将所有目标文件连接到可执行文件中。

One Definition Rule指的是要求可执行文件中使用的每个变量(和函数)仅出现在编译器创建的一个目标文件中。所有其他目标文件只有一个外部原型引用变量/函数。

模板,它是C ++的一个非常晚的添加,并且要求在每个对象的每次编译期间都可以使用所有模板实现细节,这样编译器就可以完成所有这些优化 - 这涉及大量的内联甚至更多的名称修改

我希望这能回答你的问题,因为它是ODR规则的原因,以及它为什么不影响模板。因为链接器几乎与模板无关,所以它们都由编译器管理。排除案例的是使用模板专门化将整个模板扩展推送到一个目标文件中,因此如果它们只看到模板的原型,它可以在其他目标文件中使用。

修改

回到过去,联系人经常链接用不同语言创建的目标文件。链接ASM和C是很常见的,即使在C ++之后仍然使用了一些代码并且绝对需要ODR。仅仅因为你的项目只链接C ++文件并不意味着所有链接器都可以做,因此它不会被更改,因为大多数项目现在只是C ++。即便是现在许多设备驱动程序根据它的原始用途使用链接器。

<强>答案:

  

然而,似乎存在那些“复杂的链接器规则”   在模板的情况下使用,为什么不在简单的情况下呢?

编译器管理模板案例,只创建弱链接器引用。

链接器 nothing 与模板有关,它们是编译器使用的模板,用于创建传递给链接器的代码。

因此链接器规则不受模板的影响,但链接器规则仍然很重要,因为ODR是ASM和C的要求,链接器仍然链接,而您以外的人仍然实际使用。

答案 1 :(得分:1)

好的,以下示例代码演示了强和弱链接器引用之间的区别。在我尝试解释为什么在2之间进行更改可以改变链接器创建的结果可执行文件之后。

prototypes.h

class CLASS
{
public:
    static const int global;
};
template <class T>
class TEMPLATE
{
public:
    static const int global;
};

void part1();
void part2();

file1.cpp

#include <iostream>
#include "template.h"
const int CLASS::global = 11;
template <class T>
const int TEMPLATE<T>::global = 21;
void part1()
{
    std::cout << TEMPLATE<int>::global << std::endl;
    std::cout << CLASS::global << std::endl;
}

file2.cpp

#include <iostream>
#include "template.h"
const int CLASS::global = 21;
template <class T>
const int TEMPLATE<T>::global = 22;
void part2()
{
    std::cout << TEMPLATE<int>::global << std::endl;
    std::cout << CLASS::global << std::endl;
}

的main.cpp

#include <stdio.h>
#include "template.h"
void main()
{
    part1();
    part2();
}

我接受这个例子是完全做作的,但希望它能说明为什么“改变强弱的链接器引用是一个突破性的改变”。

这会编译吗?不,因为它有2个强引用CLASS :: global。

如果删除对CLASS :: global的强引用之一,它会编译吗?是

TEMPLATE :: global的价值是什么?

CLASS :: global的价值是什么?

弱引用是 undefined ,因为它取决于链接顺序,这使得它最多模糊,并且取决于链接器不可控制。这可能是可以接受的,因为不将所有模板保存在单个文件中是不常见的,因为原型和实现都需要一起进行编译才能工作。

但是,对于类静态数据成员,因为它们是历史上的强引用,并且在声明中不可定义,所以这是规则,现在至少通常的做法是在实现文件中使用强引用进行完整数据声明。

事实上,由于链接器会因违反强引用而产生ODR链接错误,因此通常会有多个目标文件(要链接的编译单元),它们是有条件地链接以改变不同硬件和软件组合的行为有时为了优化的好处。知道你的链接参数是否犯了错误,你会得到一个错误:要么你忘记选择专业化(没有强引用),要么选择了多个专业化(多个强引用)

你需要记住在引入C ++时,8位,16位和32位处理器都是有效的目标,AMD和英特尔有类似但不同的指令集,硬件供应商更喜欢封闭的专用接口来开放标准。构建周期可能需要数小时,数天甚至一周。