【问题标题】:Why does default constructor of std::atomic not default initialize the underlying stored value?为什么 std::atomic 的默认构造函数不默认初始化底层存储值?
【发布时间】:2020-03-24 17:35:53
【问题描述】:

由于今天是美国的感恩节,我将成为指定的火鸡来问这个问题:

拿些像这样无害的东西。具有简单普通旧数据类型(例如 int)的原子:

atomic<int> x;
cout << x;

上面将打印出垃圾(未定义)数据。考虑到我为atomic constuctor 阅读的内容,这是有道理的:

(1) 默认构造函数

使原子对象处于未初始化状态。 未初始化的原子对象稍后可能会通过调用 atomic_init 来初始化。

感觉像是一个奇怪的委员会决定。但我相信他们有他们的理由。但是我想不出另一个std:: 类,默认构造函数将使对象处于未定义状态。

我可以看到对于没有默认构造函数并且需要使用atomic_init 路径的std::atomic 使用更复杂的类型是多么有意义。但更一般的情况是在引用计数、顺序标识符值和基于简单轮询的锁定等场景中使用具有简单类型的原子。因此,这些类型没有自己的存储值“零初始化”(默认初始化)感觉很奇怪。或者至少,如果无法预测,为什么还要使用默认构造函数。

未初始化的std::atomic 实例有用的原因是什么。

【问题讨论】:

  • 不,真正奇怪的是,atomic&lt;int&gt; i{}; 也不会对它们进行零初始化。
  • 默认的ctor需要建立一个类型的内部不变量。但是值初始化应该至少为复制建立一个有效状态,以便T x = T(); 工作。但是复制原子并没有真正的意义,因为原子不是关于基本操作行为的值(并且复制不是原子操作)。
  • std::array a;也使 a[0] 未初始化。

标签: c++ initialization language-lawyer stdatomic


【解决方案1】:

作为mentioned in P0883,这种行为的主要原因是与C的兼容性。显然C没有值初始化的概念; atomic_int i; 不执行初始化。为了与 C 兼容,C++ 等效项也必须不执行初始化。由于 C++ 中的 atomic_int 应该是 std::atomic&lt;int&gt; 的别名,因此为了完全兼容 C/C++,该类型也必须不执行任何初始化。

幸运的是,C++20 looks to be undoing this behavior

【讨论】:

  • 哇。通过削弱和窃听 C++ 版本,这对于未来与 C 的兼容性来说似乎很疯狂。我看到 Herb Sutter 已经解决了这个问题。这对我来说已经足够了。
  • 我不确定这个论点是否完全成立。没有这个,原子排序到 asm 指令的对象表示和映射可以与 C 兼容,用于在 C 和 C++ 之间传递atomic_int*。 No-init 只对 source 兼容很重要。读取未初始化的值是 UB(我认为即使是原子的?)所以在第一次读取之前必须有一个原子存储。也只与自动存储相关:静态存储是零初始化的,所以它不能应用于全局extern "C" std::atomic_int foo
  • 但是是的,既然你链接了 P0883,我不同意的不是你的答案,而是该文档引用的关于 C++ 以这种方式工作的原因。
  • @selbie 削弱了,也许吧。窃听?对于遵循与普通标量类型相同的语义,这是一个非常强烈的词。
【解决方案2】:

未初始化的std::atomic 的原因是什么 实例会很有用。

出于同样的原因,基本的“构建块”用户定义类型不应超出严格需要的范围,尤其是在构造等不可避免的操作中。

但我想不出另一个 std:: 类的默认构造函数 将使对象处于未定义状态。

所有不需要内部不变量的类都是这种情况。

在泛型代码中不期望T x; 会创建一个零初始化对象;但预计它将创建一个处于可用状态的对象。对于标量类型,任何现有对象在其生命周期内都是可用的。

另一方面,预计

T x = T();

将为通用代码创建一个处于默认状态的对象,用于普通值类型。 (如果被表示的值有这样的东西,它通常是一个“零值”。)

原子非常不同,它们存在于不同的“世界”中

Atomics 并不是关于一系列值。它们是关于为读取、写入和复杂操作提供特殊保证; 原子在很多方面都不同于其他数据类型,因为从来没有根据对该对象的正常赋值来定义复合赋值操作。所以通常的等价性不适用于原子。 你不能像对待普通对象那样推理原子。

您根本无法在原子和普通对象上编写通用代码;这是没有意义的。

(见脚注。)

总结

  • 您可以使用通用代码,但不能使用原子-非原子通用算法,因为它们的语义不属于同一语义定义风格(甚至不清楚 C++ 如何同时具有原子和非原子操作)。
  • “不用为不用的东西付费。”
  • 没有通用代码会假定未初始化的变量有值;只是它对于赋值和其他不依赖于先前值的操作处于有效状态(显然没有复合赋值)。
  • 许多 STL 类型未通过其默认构造函数初始化为“零”或默认值。

[脚注:

以下是“咆哮”,它是技术上的重要文本,但对于理解原子对象的构造函数为何如此重要并不重要。

它们只是以最深刻的方式遵循不同的语义规则:在某种程度上标准甚至没有描述,因为标准从未解释多线程的最基本事实:语言的某些部分被评估为一系列操作取得进展,而其他领域(原子,try_lock ...)则没有。 事实上,标准的作者显然甚至没有看到这种区别,甚至不理解这种二元性。 (请注意,讨论这些问题通常会使您的问题和答案被否决和删除。)

这种区别是必不可少的,因为没有它(同样,它在标准中没有出现),恰好零程序甚至可以具有多线程定义的行为:没有这种二元性只能解释旧式的前线程行为。

C++ 委员会不了解 C++ 的症状是他们认为“无稀薄空气价值”是一个额外的功能,而不是语义的重要部分(没有得到原子的“无稀薄空气”保证让顺序程序的顺序语义的承诺更加站不住脚。

--尾注]

【讨论】:

  • 正如我在回答您的问题 What formally guarantees that non-atomic variables can't see out-of-thin-air values and create a data race like atomic relaxed theoretically can? 时所解释的,很明显,C++ 标准的作者考虑将凭空产生的值是实现可以选择允许的东西。他们只是没有想出如何在没有其他方面过于强大的规则的情况下正式禁止它。这个答案开头很好,但过于戏剧性的咆哮似乎错位且不必要。
  • 关于int 本身没有默认构造函数的论点很有意义。 Nicol 的观点(在 cmets 中)关于 atomic&lt;int&gt; i{} 不是零初始化的这一论点并没有被这个论点解释,但这不是问题所要问的。
  • @PeterCordes 没有真正的实现可以拥有这些,这并不意味着被认为是可以接受的,但是标准甚至没有非正式地说它们是严格禁止的,并且在正确解释意图的情况下是不可能的。但潜在的问题是标准过于强调规范而太少强调意图。如果意图像 Stroustrup 的作品那样清晰,读者就会知道如何处理模棱两可和矛盾的地方(正如他们在实践中总是要做的那样,忽略实际的话)。标准文本的混乱对我来说是戏剧性的。
  • 总的来说,这是一个很好的观点。不过,该特定非正式要求的当前措辞相当强硬。但是这种事情一直是用 C++ 编写现实世界的软件的一个“问题”,而不仅仅是只依赖于纯 ISO C++ 构造的象牙塔代码。也许委员会可以或应该通过明确保证更多的东西来改变这种状况。
  • 为了记录,拥有 10k 代表的用户可以看到已删除的答案,因此这种方式有一点透明度。除此之外,将开始聊天。
猜你喜欢
  • 1970-01-01
  • 2017-05-31
  • 2021-12-16
  • 2015-01-12
  • 2016-05-07
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多