13

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 常见问题解答可在此处获得。相关语句是:

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

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

4

2 回答 2

1

C++ Build 结构曾经非常简单。

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

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

模板在 C++ 中添加得很晚,并且要求所有模板实现细节在每个对象的每次编译期间都可用,以便编译器可以进行所有优化 - 这涉及大量内联甚至更多名称修饰。

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

编辑:

回到过去,链接器经常链接使用不同语言创建的目标文件。链接 ASM 和 C 是很常见的,即使在 C++ 之后,仍然使用其中的一些代码,这绝对需要 ODR。仅仅因为您的项目仅链接 C++ 文件并不意味着链接器就可以做到这一点,因此它不会被更改,因为大多数项目现在都是 C++。即使是现在很多设备驱动程序都按照它的初衷来使用链接器。

回答:

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

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

链接器与模板无关,它们是编译器用来创建传递给链接器的代码的模板。

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

于 2013-09-20T08:31:43.013 回答
1

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

原型.h

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

void part1();
void part2();

文件1.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;
}

文件2.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;
}

主文件

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

我接受这个例子完全是人为的,但希望它能说明为什么“将强链接器引用更改为弱链接器引用是一个重大变化”。

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

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

TEMPLATE::global 的价值是什么?

CLASS::global 的价值是什么?

弱引用是未定义的,因为它取决于链接顺序,这使得它充其量是模糊的,并且取决于链接器不可控。这可能是可以接受的,因为不将所有模板保存在一个文件中是不常见的,因为编译工作需要原型和实现一起工作。

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

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

您需要记住,在引入 C++ 时,8 位、16 位和 32 位处理器仍然是有效的目标,AMD 和 Intel 有相似但不同的指令集,硬件供应商更喜欢封闭的私有接口而不是开放标准。构建周期可能需要数小时、数天甚至一周。

于 2013-09-21T04:12:22.573 回答