通过易失性引用/指针访问声明的非易失性对象是否会在所述访问时赋予易失性规则?

这将是一个漫长的过程,关于它的上下文并提供尽可能多的信息,我必须蜿蜒通过各种链接和引用 – 这通常是我们进入C / C ++标准兔子洞后的唯一方法。 如果您对此帖有更好的引用或任何其他改进,请告诉我。 但是要事先总结一下, 你可以责怪@ zwol发布这个;-)并且目的是从两个命题中找到真相:

  • 执行C和(通过导入;请参阅注释)C ++标准要求通过volatile *volatile &访问必须引用最初声明为volatile的对象才能具有volatile语义?
  • 或者是通过volatile指针/引用访问非易失volatile限定对象/应该使所述访问行为就像对象被声明为volatile

无论哪种方式,如果(看起来)措辞与意图相比有些含糊不清 – 我们可以在标准本身中明确说明吗?

这些相互排斥的解释中的第一个更常见,而且并非完全没有依据。 但是,我希望表明有大量的“合理怀疑”支持第二个 – 特别是当我们回到基本原理和WG论文中的一些先前的段落时。

接受的智慧:被引用的对象本身必须被声明为volatile

昨天流行的问题是“挥发性”这个波动的定义,还是GCC有一些标准的合规性问题? 通过假设一个volatile引用会给volatile volatile指示物带来volatile行为 – 但发现它没有,或者在不同程度上以不可预测的方式做出来。

接受的答案最初得出结论,只有宣称的指称类型才重要。 这个和大多数注释似乎都同意等效原则在我们对const很熟悉的情况下起作用:如果引用具有与引用对象相同的cv -qualification,则行为将只是volatile (或根本定义):

该段落中的关键词是对象。 volatile sig_atomic_t flag; 是一个易变的对象。 *(volatile char *)foo仅仅是通过volatile限定左值的访问,标准不要求具有任何特殊效果 。 – zwol

这种解释似乎被广泛持有,正如对这个相似但希望不重复的问题的回答所示: 指向易失性指向非易失性对象的行为要求但即使存在不确定性:紧接着答案是’不’,然后说’也许’! 无论如何……让我们检查一下标准,看看’没有’是什么。

标准所说的……或者不是

C11, N1548,§6.7.3 :很明显,通过不共享所述限定符的指针访问 volatileconst类型定义的对象是UB …

6如果尝试通过使用具有非const qualifiedified类型的左值来修改使用const限定类型定义的对象,则行为未定义。 如果尝试通过使用具有非volatile-限定类型的左值来引用使用volatile -qualified类型定义的对象 ,则行为未定义。(133)

…标准似乎没有明确提到相反的情况,即对于volatile 。 此外,在总结volatile和操作时,它现在讨论了一个具有 volatile -qualified类型的对象:

7 具有volatile限定类型对象可能会以实现未知的方式进行修改,或者具有其他未知的副作用。 因此,任何涉及这种对象的表达都应严格按照抽象机的规则进行评估,如5.1.2.3所述。 此外,在每个序列点,最后存储在对象中的值应与抽象机器规定的值一致,除非由前面提到的未知因素修改。(134)什么构成对具有volatile限定类型的对象的访问是实现定义。

我们是否假设“有”等同于“被定义为”? 或者“有”可以指对象和参考限定符的组合?

一位评论者用这种措辞总结了这个问题:

n1548§6.7.3¶6开始,该标准使用短语“使用volatile限定类型定义的对象”将其与“具有volatile限定类型的左值”区分开来。 不幸的是,这个“用”与“左值”区分的“对象”没有结转,然后标准使用“具有volatile限定类型的对象”,并说“构成对具有volatile限定类型的对象的访问权限”是实现定义的“(为了清楚起见,可以说”左值“或”对象定义为“)。 那好吧。 – 迪特里希·艾普

同一部分的第4段似乎不那么频繁引用,但可能很有用,我们将在下一节中看到。

合理怀疑:/是一个volatile指针/引用是否意图在其解引用上赋予volatile语义?

上述答案有一个评论,其中提交人引用委员会早些时候的声明,对“参考必须符合指称”的想法产生怀疑:

有趣的是,那里有一句话[ C99基于volatile理由 ]意味着委员会意味着 *(volatile T*)x迫使对x一次访问被视为易变; 但是标准的实际措辞没有实现这一点。 – zwol

我们可以从上面提到的第二个线程中找到有关基本原理的这一点的更多信息: 指向易失性对象的指针行为的要求

另一方面, 这篇文章引用了国际标准的基本原理6.7.3 – 编程语言 – C:

将值转换为限定类型无效; 资格(volatile,比如说)可能对访问没有影响,因为它已经发生在案例之前。 如果需要使用volatile语义访问非易失性对象,则该技术是将对象的地址强制转换为适当的指向限定类型​​的类型,然后取消引用该指针。

philipxy

从那个Bytes线程 ,我们被称为C99 s6.7.3 p3 – 又名C11的p4 – 并且这个分析:

有关段落就在理由文件第6.7.3.1节之前。 如果您还需要引用标准文档本身,请参阅6.7.3 p3:

与限定类型关联的属性仅对作为左值的表达式有意义。

表达式(volatile WHATEVER) non_volatile_object_identifier 不是左值,因此’volatile’限定符无意义。

相反,表达式* (volatile WHATEVER *) & non_volatile_object_identifier 一个左值 (它可以放在赋值语句的左侧),因此’volatile’限定符的属性在这种情况下具有其预期含义。

Tim Rentsch

在WG Paper N1381中 ,有一个非常具体的示范支持这一想法,特别是关于第一个相关问题。 这引入了附件memset_s()来做OP想要的事情 – 保证非memset_s()的内存填充。 在讨论可能的实现时,它似乎支持这个想法 – 通过省略陈述任何要求 – 使用volatile指针来改变非易失volatile对象应该基于指针限定符生成代码,而不管引用对象的类型。 ..

  1. 独立于平台的’secure-memset’解决方案:
 void *secure_memset(void *v, int c , size_t n) { volatile unsigned char *p = v; while (n--) *p++ = c; return v; } 

这种方法可以防止内存的清理被优化掉,并且它应该适用于任何符合标准的平台。

……那些没有这样做的编译器正在注意……

最近有人注意到一些编译器违反了标准,并不总是尊重volatile限定符。

谁是对的?

那令人筋疲力尽。 这里肯定有很多解释空间,这取决于你碰巧阅读哪些文件而不是哪些文件,以及你如何选择解释很多不够具体的文字。 似乎很明显有些不对劲:要么:

  • 基本原理和N1381是错误的或随意的措辞,或
  • 他们被追溯地特别无效……或者
  • 标准是错误的或随意的措辞。

我希望我们能做得比过去似乎已经包围过的所有歧义和猜测做得更好 – 并且得到一个更有说服力的声明。 为此,非常欢迎来自专家的任何进一步的资料和想法。

通过易失性引用/指针访问声明的非易失性对象是否会在所述访问时赋予易失性规则?

volatile在C&C ++中并不代表同样的东西。 C ++标准通过volatile lvalues进行访问可观察。 [1]它说它打算与C行为相同。 这就是C原理中描述的行为。 然而,C标准表示可以观察到对volatile声明的对象的访问。 (请注意,未定义通过非易失性左值访问volatile声明的对象。)

然而。 有一个缺陷报告基本上有标准应该说的委员会协议(虽然仍然是开放的),并且意图一直是,并且实施总是反映出,重要的是不是对象的波动性(根据标准)但是(访问的左值)的波动性(根据基本原理)。

C11版本1.10的缺陷报告摘要日期:2016年4月DR 476左值的易失性语义04/2016打开

当然,关于可观察行为的做法是依赖于实现的。

真的没有任何含糊之处。 只是人们无法相信 C标准行为可能是它的本质,因为这不是历史使用的预易变性(当地址文字左值被认为是易失性对象时),正如基本原理所预期的那样,由编译器在之前和之后实现,如C ++标准所解释和描述的那样,在DR中得到纠正。 同样,标准很明确,因为它没有说非易失性访问是可观察的,所以它们不是。 (“副作用”是用于定义评估部分顺序的术语。)

[1]或者至少希望现在能做到。 来自underscore_d的评论:

对于C ++,另请参阅P0612R0:NB注释CH 2:volatile , 本月采用它来清理C ++标准中关于“volatile对象”的一些剩余的讨论,当真正通过volatile glvalues进行访问时它意味着什么(因为,大概是/希望,C意味着什么)。

转换回答是因为我认为一个深思熟虑的非答案可能有助于在这里发现真相。

我想潜在的问题是“我们对内存模型的期望是多么抽象?”。 通过将非vol指针限定为volatile,我们似乎要求编译器“直接写入I / O或内存”。 没关系,但是如果编译器之前已经推断出“内存”不需要存在,它该怎么办? 回溯并创建内存,还是忽略你?

在我看来,以下两个案例的意图非常不同:

内存映射I / O.

volatile unsigned char * const uart_base = (volatile unsigned char *)0x10000;

这显然是为了通知编译器在地址0x10000处存在uart内存映射。

擦除密码哈希值

 void *secure_memset(void *v, int c , size_t n) { volatile unsigned char *p = v; while (n--) *p++ = c; return v; } 

这显然是为了确保在函数返回之前实际修改v到(int *)v + n的内存。

但是,如果推断出v中的内存从未被需要,是否可以省略对此函数的调用,这一点尚不清楚。

我认为,如果以前在程序中,已经推断出内存根本不需要存在,那么如果调用被省略,我将不会感到惊讶,无论转换为volatile。

谢谢。 因为地址被占用,是不是占用内存所需的对象?

gcc似乎同意你的观点:

 #include  #include  void * clearmem(void* p, std::size_t len) { auto vp = reinterpret_cast(p); while (len--) { *vp++ = 0; } return p; } struct A { char sensitive[100]; A(const char* p) { std::strcpy(sensitive, p); } ~A() { clearmem(&sensitive[0], 100); } }; void use_privacy(A a) { auto b = a; } int main() { A a("very private"); use_privacy(a); } 

收益率:

 clearmem(void*, unsigned long): leaq (%rdi,%rsi), %rax testq %rsi, %rsi je .L4 .L5: movb $0, (%rdi) addq $1, %rdi cmpq %rax, %rdi jne .L5 .L4: xorl %eax, %eax ret use_privacy(A): leaq -120(%rsp), %rax leaq 100(%rax), %rdx .L10: movb $0, (%rax) addq $1, %rax cmpq %rdx, %rax jne .L10 ret main: leaq -120(%rsp), %rax leaq 100(%rax), %rdx .L13: movb $0, (%rax) addq $1, %rax cmpq %rdx, %rax jne .L13 leaq -120(%rsp), %rax leaq 100(%rax), %rdx .L14: movb $0, (%rax) addq $1, %rax cmpq %rdx, %rax jne .L14 leaq -120(%rsp), %rax leaq 100(%rax), %rdx .L15: movb $0, (%rax) addq $1, %rax cmpq %rdx, %rax jne .L15 xorl %eax, %eax ret 

clang并没有忽视私人arrays的构建,因此我无法得出任何结论。

标准不会尝试定义有用实现中所需的所有行为。 该基本原理明确承认实现可以同时符合并且几乎完全无用的可能性。

标准将volatile访问的语义分类为实现定义,并且不以任何方式,形状或forms要求实现必须有效地定义它们。 因此,如果提供记录和实际行为一致,那么像gcc那样的volatile语义的实现会使实现不符合,但仅仅使其无法用于其他方面可能适用的目的,这是不合理的。

请注意,gcc通常用在可能配置任意区域的地址空间的平台上,其行为有点像I / O设备,然后表现得像普通的RAM。 因此,即使大多数其他操作的顺序无关紧要,也可能需要确保对某些操作进行非常精确的排序; 要求对某些事物的所有操作必须被视为volatile ,以便进行如此处理的任何操作似乎不是优化的好方法。

我发现奇怪的是,过去几年人们对标准是否允许编译器为某些构造实现无用语义,以改善不需要构造有用的代码性能的目的感兴趣,当这样的时候默认情况下,一种方法在几乎所有方面看起来都不如实现有用的语义,但是提供不需要这种语义的程序员可以使用命令行开关或#pragma指令来放弃它们。 如果一个程序包含一个[假设的] #pragma gcc_loose_volatile指令,gcc可以做任何它喜欢的事情,无论人们怎么解释标准关于volatile的要求,如果它不包含这样的指令,那么无用的语义就没用了标准是否禁止它们。

对象是易失性的还是非易失性的。 使用volatile引用引用对象将生成无论对象是否为volatile的正确代码。