名稱空間
變體
操作

as-if 規則

來自 cppreference.com
< 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 文件 關於 “彷彿”規則