命名空间
变体
操作

as-if 规则

来自 cppreference.cn
< cpp‎ | 语言
 
 
C++ 语言
 
 

允许任何和所有不改变程序可观察行为的代码转换。

目录

[编辑] 解释

程序的可观察行为包括以下内容

  • 在每个序列点,所有volatile对象的值都是稳定的(之前的求值已完成,新的求值尚未开始)。
(C++11 前)
  • volatile对象的访问(读和写)严格按照它们所出现的表达式的语义进行。特别是,它们不会相对于同一线程上的其他volatile访问被重新排序
(C++11 起)
  • 在程序终止时,写入文件的数据与程序按原样执行时完全相同。
(直到 C++26)
  • 传递给宿主环境的数据被写入文件。
(C++26 起)
  • 发送到交互式设备的提示文本将在程序等待输入之前显示。
  • 如果支持 ISO C pragma #pragma STDC FENV_ACCESS 并将其设置为 ON,则对浮点环境(浮点异常和舍入模式)的更改保证会被浮点算术运算符和函数调用观察到,就像它们按原样执行一样,但以下情况除外:
    • 除类型转换和赋值之外的任何浮点表达式的结果可能具有与表达式类型不同的浮点类型的范围和精度(参见 FLT_EVAL_METHOD),
    • 尽管有上述规定,任何浮点表达式的中间结果可以像无限范围和精度那样计算(除非 #pragma STDC FP_CONTRACTOFF)。

C++ 编译器允许对程序执行任何更改,只要在给定相同输入的情况下,程序的 observable behavior 是与该输入对应的可能 observable behaviors 之一。

然而,如果某些输入会导致未定义行为,则编译器无法保证程序在该输入下的任何可观察行为,即使任何可观察行为的操作发生在任何可能的未定义操作之前。

(直到 C++26)

程序可能包含可观察检查点

如果对于每个未定义操作 U,都存在一个可观察检查点 CP,使得 OP 发生在 CP 之前,且 CP 发生在 U 之前,则操作 OP无未定义行为的。程序在给定输入下的已定义前缀包含其所有无未定义行为的操作。

C++ 编译器允许对程序执行任何更改,只要在给定相同输入的情况下,程序的已定义前缀的可观察行为是与该已定义前缀对应的可能可观察行为之一。

如果某些输入会导致未定义行为,则编译器无法保证程序在该输入下不属于已定义前缀的任何可观察行为。

(C++26 起)

[编辑] 注意

由于编译器(通常)无法分析外部库的代码以确定它是否执行 I/O 或 volatile 访问,因此第三方库调用也不受优化影响。然而,标准库调用可能会在优化期间被其他调用替换、消除或添加到程序中。静态链接的第三方库代码可能会受到链接时优化的影响。

具有未定义行为的程序在用不同的优化设置重新编译时通常会改变可观察行为。例如,如果对有符号整数溢出的测试依赖于溢出结果,例如 if (n + 1 < n) abort();它被某些编译器完全删除,因为有符号溢出是未定义行为,并且优化器可以自由地假设它永远不会发生,并且测试是冗余的。

拷贝消除是“仿佛”规则的一个例外:编译器可以移除对移动构造函数和拷贝构造函数的调用,以及对临时对象的析构函数的匹配调用,即使这些调用具有可观察的副作用。

new 表达式是“仿佛”规则的另一个例外:编译器可以移除对可替换分配函数的调用,即使提供了用户定义的替换并具有可观察的副作用。

(C++14 起)

浮点异常的计数和顺序可以通过优化改变,只要下一个浮点操作所观察到的状态就像没有发生优化一样

#pragma STDC FENV_ACCESS ON
for (i = 0; i < n; ++i)
    x + 1; // x + 1 is dead code, but may raise FP exceptions
           // (unless the optimizer can prove otherwise). However, executing it n times
           // will raise the same exception over and over. So this can be optimized to:
if (0 < n)
    x + 1;

[编辑] 示例

int& preinc(int& n) { return ++n; }
int add(int n, int m) { return n + m; }
 
// volatile input to prevent constant folding
volatile int input = 7;
 
// volatile output to make the result a visible side-effect
volatile int result;
 
int main()
{
    int n = input;
// using built-in operators would invoke undefined behavior
//  int m = ++n + ++n;
// but using functions makes sure the code executes as-if 
// the functions were not overlapped
    int m = add(preinc(n), preinc(n));
    result = m;
}

输出

# full code of the main() function as produced by the GCC compiler
# x86 (Intel) platform:
        movl    input(%rip), %eax   # eax = input
        leal    3(%rax,%rax), %eax  # eax = 3 + eax + eax
        movl    %eax, result(%rip)  # result = eax
        xorl    %eax, %eax          # eax = 0 (the return value of main())
        ret
 
# PowerPC (IBM) platform:
        lwz 9,LC..1(2)
        li 3,0          # r3 = 0 (the return value of main())
        lwz 11,0(9)     # r11 = input;
        slwi 11,11,1    # r11 = r11 << 1;
        addi 0,11,3     # r0 = r11 + 3;
        stw 0,4(9)      # result = r0;
        blr
 
# Sparc (Sun) platform:
        sethi   %hi(result), %g2
        sethi   %hi(input), %g1
        mov     0, %o0                 # o0 = 0 (the return value of main)
        ld      [%g1+%lo(input)], %g1  # g1 = input
        add     %g1, %g1, %g1          # g1 = g1 + g1
        add     %g1, 3, %g1            # g1 = 3 + g1
        st      %g1, [%g2+%lo(result)] # result = g1
        jmp     %o7+8
        nop
 
# in all cases, the side effects of preinc() were eliminated, and the
# entire main() function was reduced to the equivalent of result = 2 * input + 3;

[编辑] 参见

C 文档 关于 “仿佛”规则