【问题标题】:Where does the standard define that a volatile variable can change?标准在哪里定义了 volatile 变量可以更改?
【发布时间】:2018-07-23 12:19:28
【问题描述】:

标准在哪里定义了 volatile 变量可以更改 未被发现?

我发现了两个关于 volatile 的规范性文本:

intro.execution/7:

读取由 volatile glvalue ([basic.lval]) 指定的对象、修改对象、调用库 I/O 函数或调用执行任何这些操作的函数都是副作用,即执行环境状态的变化。表达式(或子表达式)的评估通常包括值计算(包括确定对象的身份以进行右值评估和获取先前分配给对象以进行纯右值评估的值)和副作用的启动。当对库 I/O 函数的调用返回或评估通过 volatile 左值的访问时,副作用被认为是完整的,即使调用(例如 I/O 本身)或 volatile 访问隐含的一些外部操作可能还没有完成。

这一段是关于未检测到的变化吗? 副作用是这个意思吗?


或者有dcl.type.cv/5

通过 volatile 左值访问的语义是实现定义的。如果尝试通过使用非易失性泛左值访问使用 volatile 限定类型定义的对象,则行为未定义。

这一段是关于我的问题吗? “通过 volatile glvalue 访问的语义是实现定义的”到底是什么意思?你能举一个不同的“访问语义”的例子吗?


还有dcl.type.cv/6,这是关于我的问题,但它只是一个注释:

[ 注意: volatile 是对实现的提示,以避免涉及对象的激进优化,因为对象的值可能会通过实现无法检测的方式进行更改。此外,对于某些实现,易失性可能表示需要特殊的硬件指令才能访问该对象。有关详细语义,请参见 [intro.execution]。一般来说,volatile 的语义在 C++ 中与在 C 中的语义相同。— 尾注 ]

【问题讨论】:

  • 您只是出于好奇而问吗?还是有一些潜在的原因?如果是后者,您遇到的realactual 问题是什么?也许你应该更直接地问这个问题?
  • @Someprogrammerdude:我想对我自己以前的question 给出一个答案,我想更好地理解这个问题。所以没有真正的底层编程问题,但出于好奇,为了更好地理解易失性:易失性变量如何以及何时可以“未被发现”的变化。
  • 我很确定这个问题和上一个问题已经在 SO 上进行了广泛讨论。
  • “通过 volatile glvalue 访问的语义是实现定义的”非常不清楚。但我认为应该阅读有关“本文档中的语义描述定义参数化的非确定性抽象机器”。因此,我认为读取一个 volatile 变量可以改变任何事情。甚至非易失变量的值。只要行为被准确记录。
  • This 可能是相关的。

标签: c++ language-lawyer c++17 volatile


【解决方案1】:

这里的关键是“执行环境状态的变化”。

执行环境是你程序之外的东西。这可能包括操作系统、文件系统、屏幕等。它通常是不可预测的。您不能假设如果您将 0 写入文件,则该文件不会被具有 1 的另一个进程覆盖。

volatile 变量在逻辑上是该执行环境的一部分。就 C++ 而言,环境可以枚举、读取和写入它们,就像文件一样。这可能会在您的程序不知情的情况下发生。

另一方面,您的实现实现了程序与其执行环境之间的链接,因此它确实对可能发生的情况有所了解。如果它有某种私有 RAM 磁盘实现,那么它可能知道某些文件名在 OS 文件系统中是不可见的。它可能知道volatile int i 存在于CPU 寄存器中,因此无法通过内存映射访问它。这都是 C++ 标准所允许的。它只是笼统地谈论执行环境,实现必须更精确。这就是“实现定义的语义”的意思。

【讨论】:

  • 我不认为有任何东西可以使普通的 volatile int i 变成与理智的编译器中的普通 int 不同的任何东西。至少在 gcc 中,您必须使用扩展并将变量声明为 asm 寄存器以使其成为 cpu 寄存器。我的意思是编译器将具有特殊的扩展名和语法来将变量声明为 cpu 寄存器,而不仅仅是神奇地使用名称或使每个 volatile int 都成为特殊的 cpu 寄存器。因此,真实的“生活在 CPU 中”的变量在源代码中看起来会有所不同。
  • @GoswinvonBrederlow 不是 CPU 寄存器。 IO 硬件寄存器。这些是完全不同的东西。没有人说 volatile 将变量映射到 CPU 寄存器(我们曾经有 register 关键字,但它已经失效多年了)。
  • @GoswinvonBrederlow 当然很容易看出 volatile 变量与任何理智编译器的常规变量都非常不同。看看汇编输出就足够了。参见例如this
  • @n.m.:我没有指定任何一种类型;在这种情况下,“注册”只是意味着“不存在于 C++ 指针可以指向的通常地址空间中”。至于“GCC 做了什么”,这个问题被标记为 language-lawyer,因此您需要考虑所有实现变化。这个问题特别依赖于实现,因为它讨论了变化很大的外部执行环境。
  • @MSalters "register" in this context just meant "doesn't live in the usual address space where C++ pointers can point to". IO 硬件寄存器绝对确实存在于 C++ 指针可以指向的通常地址空间中。这就是内存映射 IO 的全部意义所在。否则它不会被称为“内存映射”。
【解决方案2】:

volatile 只是对编译器的请求,要求它为每次访问从内存中重新加载变量。它适用于 2 个常见用例:

  • 变量可以被不同的线程(甚至是可以被授予对该内存区域的写访问权限的不同程序)或内核模式代码(例如通过特殊的驱动程序)进行更改
  • 这个变量代表一个物理内存寄存器,主要用于内核模式编程,或者在没有用户/内核模式概念的操作系统上,比如老式的 MS/DOS。

一旦您知道这一点,标准中的不同引用都是有意义的。

读取一个由 volatile glvalue ([basic.lval]) 指定的对象,...都是副作用,是执行环境状态的变化。

读取硬件寄存器可能会对底层系统产生影响,这就是为什么说它是可观察到的副作用

通过 volatile 左值访问的语义是实现定义的。如果尝试通过使用非易失性泛左值访问使用 volatile 限定类型定义的对象,则行为未定义。

如果使用非易失性指针访问易失性硬件寄存器,编译器可以缓存之前的值而不执行物理访问。

[ 注意: volatile 是对实现的提示,以避免涉及对象的激进优化,因为对象的值可能会通过实现无法检测的方式进行更改。此外,对于某些实现,易失性可能表示需要特殊的硬件指令才能访问该对象。有关详细语义,请参见 [intro.execution]。一般来说,volatile 的语义在 C++ 中与在 C 中的语义相同。— 尾注 ]

某些实现可以为特殊的低级 io 端口操作保留一个内存区域。在这种情况下,可能需要 volatile 说明符和特殊内存区域地址的组合来验证转换或具有特殊 io 操作的正常内存访问操作。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2015-02-03
    • 2018-06-14
    • 2010-11-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多