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++允许在类内定义需要作为对象存储在内存中的实体,则该规则将被打破
然而,似乎那些"复杂的链接器规则"确实存在并在模板案例中使用,那么为什么不在简单情况下呢?
好的,下面的示例代码演示了强链接器引用和弱链接器引用之间的区别。之后我将尝试解释为什么在 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;
}
主.cpp
#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 和英特尔具有相似但不同的指令集,硬件供应商更喜欢封闭的专用接口而不是开放标准。 构建周期可能需要数小时、数天甚至一周。
C++ 构建结构过去非常简单。
编译器构建了通常包含一个类实现的对象文件。然后,链接器将所有目标文件联接到可执行文件中。
一个定义规则是指可执行文件中使用的每个变量(和函数)仅出现在编译器创建的一个对象文件中的要求。 所有其他对象文件都只是对变量/函数的外部原型引用。
模板,其中非常晚添加到C++,并要求在每个对象的每次编译期间所有模板实现详细信息都可用,以便编译器可以执行其所有优化 - 这涉及大量内联甚至更多的名称重整。
我希望这能回答您的问题,因为它是 ODR 规则的原因,以及为什么它不会影响模板。 由于链接器与模板几乎没有关系,因此它们都由编译器管理。 排除这种情况是使用模板专用化将整个模板扩展推送到一个对象文件中,因此可以在其他对象文件中使用它,如果他们只看到模板的原型。
编辑:
在过去,链接器经常链接使用不同语言创建的对象文件。 链接ASM和C是很常见的,即使在C++之后,一些代码仍然被使用,这绝对需要ODR。 仅仅因为您的项目仅链接C++文件并不意味着这是链接器可以执行的所有操作,因此不会更改它,因为大多数项目现在都是C++的。 即使是现在,许多设备驱动程序也根据其更原始的意图使用链接器。
答:
然而,似乎这些"复杂的链接器规则"确实存在,并且 用于模板案例,那么为什么不在简单案例中使用呢?
编译器管理模板案例,并仅创建弱链接器引用。
链接器与模板无关,它们是编译器用来创建传递给链接器的代码的模板。
因此,链接器规则不受模板的影响,但链接器规则仍然很重要,因为 ODR 是 ASM 和 C 的要求,链接器仍然链接,并且除了您之外的人仍然实际使用。