求值順序
來自 cppreference.com
任何表示式的任何部分的求值順序,包括函式實參的求值順序,都是未指明的(除下面列出的某些例外情況外)。編譯器可以以任何順序求值運算元及其他子表示式,並且在再次求值同一表示式時可以選擇另一順序。
在 C++ 中沒有從左到右或從右到左求值的概念。這不應與運算子的從左到右和從右到左的結合性相混淆:表示式 a() + b() + c() 因為 operator+ 的從左到右結合性而被解析成 (a() + b()) + c(),但在執行時,c() 可能被最先、最後或在 a() 或 b() 之間求值。
執行此程式碼
可能的輸出
b c a c a b
目錄 |
[編輯] “ Sequenced before ”規則 (C++11 起)
[編輯] 表示式求值
每個表示式的求值包括:
- 值計算:計算由表示式返回的值。這可能涉及確定物件的標識(泛左值求值,例如表示式返回對某個物件的引用),或讀取先前賦給物件的值(純右值求值,例如表示式返回一個數字或其他值)。
- 副作用的引發:訪問(讀或寫)由 volatile 泛左值所指代的的物件,修改(寫入)物件,呼叫一個庫 I/O 函式,或呼叫一個進行任何這些操作的函式。
[編輯] 定序
Sequenced before(定序於……之前)是同一執行緒中求值 A
和 B
之間的非對稱、可傳遞的成對關係。
- 若
A
sequenced beforeB
(或者等價地,B
sequenced afterA
),則A
的求值將在B
的求值開始前完成。 - 若
A
不 sequenced beforeB
且B
sequenced beforeA
,則B
的求值將在A
的求值開始前完成。 - 若
A
不 sequenced beforeB
且B
不 sequenced beforeA
,則存在兩種可能性:A
和B
的求值是無序的 (unsequenced):它們可以按任意順序執行並且可能重疊(在單個執行執行緒中,編譯器可以交錯構成A
和B
的 CPU 指令)。A
和B
的求值是順序不確定的 (indeterminately sequenced):它們可以按任意順序執行但不能重疊:要麼A
在B
之前完成,要麼B
在A
之前完成。下次求值同一表示式時,順序可能相反。
如果與表示式 X 相關聯的每個值計算和每個副作用都定序於與表示式 Y 相關聯的每個值計算和每個副作用之前,則稱表示式 X 定序於表示式 Y 之前。
[編輯] 規則
1) 每個完整表示式都定序於下一個完整表示式之前。
2) 任何運算子的運算元的值計算(而非其副作用)都定序於該運算子結果的值計算(而非其副作用)之前。
3) 呼叫函式 func 時(無論函式是否為內聯,以及是否使用顯式函式呼叫語法),下列列表中的每一項都定序於下一項之前:
- 每個實參表示式和指代 func 的字尾表示式
|
(C++26 起) |
- func 函式體中的每個表示式或語句
|
(C++26 起) |
4) 內建字尾自增和字尾自減運算子的值計算定序於其副作用之前。
5) 內建字首自增和字首自減運算子的副作用定序於其值計算之前(由於定義為複合賦值的隱式規則)。
9) 在列表初始化中,給定初始化器子句的每個值計算和副作用,都定序於花括號包圍的逗號分隔的初始化器列表中,任何在其後的初始化器子句的所有值計算和副作用之前。
10) 一個既不定序於函式外的另一個表示式求值之前也不定序於其後的函式呼叫(可能是另一個函式呼叫),相對於該求值是順序不確定的(程式必須表現得如同構成一個函式呼叫的 CPU 指令不與構成其他表示式(包括其他函式呼叫)求值的指令交錯,即使函式被內聯)。
規則 10 有一個例外:在 std::execution::par_unseq 執行策略下由標準庫演算法進行的函式呼叫是無序的,並且可以任意地相互交錯。 | (C++17 起) |
13) 在函式呼叫表示式中,指名函式的表示式定序於每個實參表示式和每個預設實參之前。
14) 在函式呼叫中,每個形參的初始化的值計算和副作用,相對於任何其他形參的值計算和副作用,都是順序不確定的。
15) 每個過載的運算子,當使用運算子表示法呼叫時,遵循它所過載的內建運算子的定序規則。
16) 在下標表達式 E1[E2] 中,E1 定序於 E2 之前。
17) 在成員指標表示式 E1.*E2 或 E1->*E2 中,E1 定序於 E2 之前(除非 E1 的動態型別不包含 E2 所引用的成員)。
18) 在移位運算子表示式 E1 << E2 和 E1 >> E2 中,E1 定序於 E2 之前。
19) 在每個簡單賦值表示式 E1 = E2 和每個複合賦值表示式 E1 @= E2 中,E2 定序於 E1 之前。
20) 在帶括號的初始化器中的逗號分隔的表示式列表中的每個表示式,其求值如同函式呼叫(順序不確定)。 |
(C++17 起) |
[編輯] 未定義行為
在下列情況下,行為是未定義的:
1) 對記憶體位置的副作用相對於同一記憶體位置的另一個副作用是無序的。
i = ++i + 2; // well-defined i = i++ + 2; // undefined behavior until C++17 f(i = -2, i = -2); // undefined behavior until C++17 f(++i, ++i); // undefined behavior until C++17, unspecified after C++17 i = ++i + i++; // undefined behavior
2) 對記憶體位置的副作用相對於使用同一記憶體位置中任何物件的值進行的值計算是無序的。
cout << i << i++; // undefined behavior until C++17 a[i] = i++; // undefined behavior until C++17 n = ++i + i; // undefined behavior
3) 在記憶體位置中開始或結束物件的生存期,相對於下列任何操作是無序的:
- 對同一記憶體位置的副作用
- 使用同一記憶體位置中任何物件的值進行的值計算
- 開始或結束佔用與該記憶體位置重疊的儲存空間的物件的生存期
union U { int x, y; } u; (u.x = 1, 0) + (u.y = 2, 0); // undefined behavior
[編輯] 序列點規則 (C++11 前)
[編輯] C++11 前的定義
表示式的求值可能產生副作用,包括:訪問由 volatile 左值所指代的物件,修改物件,呼叫一個庫 I/O 函式,或呼叫一個進行任何這些操作的函式。
序列點是執行序列中的一個點,在此點所有來自序列中先前求值的副作用都已完成,並且後續求值的副作用都尚未開始。
[編輯] C++11 前的規則
1) 在每個完整表示式的末尾有一個序列點(通常在分號處)。
2) 當呼叫一個函式時(無論函式是否為內聯,也無論是否使用了函式呼叫語法),在所有函式實參(如果有)求值之後,並且在函式體中任何表示式或語句執行之前,有一個序列點。
4) 在複製一個函式的返回值之後,以及在函式外任何表示式執行之前,有一個序列點。
5) 一旦函式開始執行,在被呼叫函式完成執行之前,不會求值任何來自呼叫函式的表示式(函式不能交錯執行)。
6) 在求值下列四個表示式中的每一個時,使用內建(非過載)運算子,在表示式 a 求值之後有一個序列點。
a && b a || b a ? b : c a , b
[編輯] C++11 前的未定義行為
在下列情況下,行為是未定義的:
1) 在前一個和下一個序列點之間,記憶體位置中任何物件的值被表示式的求值修改超過一次。
i = ++i + i++; // undefined behavior i = i++ + 1; // undefined behavior i = ++i + 1; // undefined behavior ++ ++i; // undefined behavior f(++i, ++i); // undefined behavior f(i = -1, i = -1); // undefined behavior
2) 在前一個和下一個序列點之間,對於一個其值被表示式求值修改的物件,以確定要儲存的值之外的方式訪問其先前的值。
cout << i << i++; // undefined behavior a[i] = i++; // undefined behavior
[編輯] 缺陷報告
下列更改行為的缺陷報告追溯地應用於以前出版的 C++ 標準。
缺陷報告 | 應用於 | 釋出時的行為 | 正確的行為 |
---|---|---|---|
CWG 1885 | C++11 | 函式返回時自動 變數銷燬的順序沒有明確說明 |
添加了定序規則 |
CWG 1949 | C++11 | “sequenced after” 在 C++ 標準中被使用但未定義 | 定義為 “sequenced before” 的反義 |
CWG 1953 | C++11 | 涉及記憶體位置的副作用和值計算 可能相對於在同一記憶體位置中開始或結束 物件的生存期是無序的 |
在這種情況下,行為是 未定義的 |
CWG 2146 | C++98 | 涉及未定義行為的情況未考慮位域 | 已考慮 |
[編輯] 參考
- C++23 標準 (ISO/IEC 14882:2024)
- 6.9.1 程式執行 [intro.execution]
- 7.6.1.6 自增和自減 [expr.post.incr]
- 7.6.2.8 New [expr.new]
- 7.6.14 邏輯與運算子 [expr.log.and]
- 7.6.15 邏輯或運算子 [expr.log.or]
- 7.6.16 條件運算子 [expr.cond]
- 7.6.19 賦值和複合賦值運算子 [expr.ass]
- 7.6.20 逗號運算子 [expr.comma]
- 9.4.5 列表初始化 [dcl.init.list]
- C++20 標準 (ISO/IEC 14882:2020)
- 6.9.1 程式執行 [intro.execution]
- 7.6.1.5 自增和自減 [expr.post.incr]
- 7.6.2.7 New [expr.new]
- 7.6.14 邏輯與運算子 [expr.log.and]
- 7.6.15 邏輯或運算子 [expr.log.or]
- 7.6.16 條件運算子 [expr.cond]
- 7.6.19 賦值和複合賦值運算子 [expr.ass]
- 7.6.20 逗號運算子 [expr.comma]
- 9.4.4 列表初始化 [dcl.init.list]
- C++17 標準 (ISO/IEC 14882:2017)
- 4.6 程式執行 [intro.execution]
- 8.2.6 自增和自減 [expr.post.incr]
- 8.3.4 New [expr.new]
- 8.14 邏輯與運算子 [expr.log.and]
- 8.15 邏輯或運算子 [expr.log.or]
- 8.16 條件運算子 [expr.cond]
- 8.18 賦值和複合賦值運算子 [expr.ass]
- 8.19 逗號運算子 [expr.comma]
- 11.6.4 列表初始化 [dcl.init.list]
- C++14 標準 (ISO/IEC 14882:2014)
- 1.9 程式執行 [intro.execution]
- 5.2.6 自增和自減 [expr.post.incr]
- 5.3.4 New [expr.new]
- 5.14 邏輯與運算子 [expr.log.and]
- 5.15 邏輯或運算子 [expr.log.or]
- 5.16 條件運算子 [expr.cond]
- 5.17 賦值和複合賦值運算子 [expr.ass]
- 5.18 逗號運算子 [expr.comma]
- 8.5.4 列表初始化 [dcl.init.list]
- C++11 標準 (ISO/IEC 14882:2011)
- 1.9 程式執行 [intro.execution]
- 5.2.6 自增和自減 [expr.post.incr]
- 5.3.4 New [expr.new]
- 5.14 邏輯與運算子 [expr.log.and]
- 5.15 邏輯或運算子 [expr.log.or]
- 5.16 條件運算子 [expr.cond]
- 5.17 賦值和複合賦值運算子 [expr.ass]
- 5.18 逗號運算子 [expr.comma]
- 8.5.4 列表初始化 [dcl.init.list]
[編輯] 參閱
- 運算子優先順序,其定義瞭如何從源程式碼表示構建表示式。
C 文件 中關於求值順序
|