【问题标题】:Why does in-class initialisation of static members violate the ODR?为什么静态成员的类内初始化会违反 ODR?
【发布时间】:2013-09-25 10:03:29
【问题描述】:

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&lt;0&gt;,编译器/链接器的魔法就会发挥作用,我会在最终的可执行文件中得到一份TemplateExample&lt;0&gt;::str 的副本。

所以我的问题是,鉴于编译器显然可以解决模板类的静态成员的 ODR 问题,为什么它不能对非模板类也这样做?

编辑:可以使用 Stroustrup 常见问题解答 here。相关句子是:

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

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

【问题讨论】:

  • C++11 放宽了这个限制。您可以使用常量表达式进行类内初始化。
  • 是的,但我的理解是,即便如此,它也需要一个定义(没有初始化器)出现在命名空间范围内的一个翻译单元中;但是在模板情况下,它可能出现在标题中,因此出现在多个 TU 中。我的问题是为什么模板案例中使用的符号合并魔法也不能用于普通的非模板类。
  • 模板不是原始 C++ 语言的一部分。
  • C++17 允许内联初始化静态数据成员(即使对于非整数类型):inline static int x[] = {1, 2, 3};。请参阅 en.cppreference.com/w/cpp/language/static#Static_data_members

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


【解决方案1】:

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

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

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

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

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

编辑:

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

答案:

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

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

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

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

【讨论】:

  • “链接器几乎与模板无关”——这并不完全正确。如果编译器将它们标记为可合并,则链接器会合并重复的函数和数据。模板实例化(函数和静态数据)这么标记,为什么非模板静态成员没有?
  • @n.m.没错,说得好
  • 究竟什么样的旧代码会被破坏,以什么方式破坏?我看不出有任何破损的原因。
  • 我不确定你的点是什么。 没有人会改变 C++ 编译模型 — 不需要改变“模型”,无论它是什么。当前的链接器非常有能力做我们想做的事。我们只需要让编译器发出正确类型的符号,它们在模板的情况下已经这样做了。 如果您不知道如何滥用 ODR — 滥用是我的中间名,我不是在问您如何做到这一点。我在问根据提议的规则会破坏哪些代码以及如何破坏。出于某种原因,您似乎对旧代码会被破坏很有信心,而我看不到那个原因。
  • @strings:这种态度是不必要的,我只是问为什么这个特定的 C++ 工具有它所做的限制,因为它(至少对于非委员会成员)似乎没有必要.由于@n.m 的原因,我没有接受你的回答。给出:即我是在一个特定的原因之后,为什么标准不要求将非模板静态数据成员符号标记为 weak,就像模板化静态数据成员一样。破解旧代码似乎不太可能,因为我真的无法从 Fortran 引用 SimpleExample::str
【解决方案2】:

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

原型.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 和 Intel 有相似但不同的指令集,硬件供应商更喜欢封闭的私有接口而不是开放标准.构建周期可能需要数小时、数天甚至一周。

【讨论】:

  • 感谢您的详细回答。我想它基本上可以归结为历史——静态数据成员过去(现在仍然是)基本上只是具有一些编译时访问限制的 C 全局变量,现在改变它为时已晚。
猜你喜欢
  • 2023-03-18
  • 2013-10-19
  • 1970-01-01
  • 2021-11-29
  • 1970-01-01
  • 2011-07-18
  • 2015-05-18
  • 2011-01-19
相关资源
最近更新 更多