未定義的行爲和順序點

什麼是「序列點」? express

未定義行爲與序列點之間有什麼關係? 併發

我常用有趣和複雜的表達式,例如a[++i] = i; ,讓本身感受好些。 爲何我應該中止使用它們? ide

若是您已閱讀此書,請務必訪問後續問題未定義行爲和重載序列點函數

(注意:這原本是Stack Overflow的C ++ FAQ的條目。若是您想批評以這種形式提供FAQ的想法,那麼在全部這些開始的meta上的張貼將是這樣作的地方。該問題在C ++聊天室中進行監控,該問題最初是從FAQ想法開始的,因此提出這個想法的人極可能會讀懂您的答案。) lua


#1樓

C ++ 98和C ++ 03

此答案適用於C ++標準的較舊版本。 該標準的C ++ 11和C ++ 14版本沒有正式包含「序列點」。 操做是「先於」或「未排序」或「不肯定地排序」。 最終效果基本相同,可是術語不一樣。 spa


免責聲明 :好的。 這個答案有點長。 所以閱讀時要有耐心。 若是您已經知道這些事情,那麼再次閱讀它們不會使您發瘋。 線程

先決條件C ++標準的基礎知識 翻譯


什麼是序列點?

標準說 code

在執行序列中某些特定的點(稱爲順序點)上 ,之前評估的全部反作用都應完整,而且之後評估的反作用都不該發生。 (第1.9 / 7節) 對象

反作用? 有什麼反作用?

對錶達式的求值會產生某些結果,而且若是執行環境的狀態另外發生變化,則能夠說該表達式(其求值)會產生一些反作用。

例如:

int x = y++; //where y is also an int

除了初始化操做以外,因爲++運算符的反作用, y的值也會更改。

到如今爲止還挺好。 繼續到序列點。 comp.lang.c做者Steve Summit給出的seq-point的交替定義:

順序點是粉塵沉澱的時間點,能夠保證到目前爲止已經看到的全部反作用都是完整的。


C ++標準中列出的常見序列點是什麼?

那些是:

  • 在對完整表達式的評估結束時(第§1.9/16 )(完整表達式是否是另外一個表達式的子表達式的表達式。) 1

    範例:

    int a = 5; // ; is a sequence point here
  • 在對第一個表達式求值以後對如下每一個表達式求值(第§1.9/182

    • a && b (§5.14)
    • a || b (§5.15)
    • a ? b : c (§5.16)
    • a , b (§5.18) (此處a,b是逗號運算符;在func(a,a++) ,它不是逗號運算符,它只是參數aa++之間的分隔符。所以,這種狀況下的行爲是不肯定的(若是a被認爲是原始類型)
  • 在對全部函數參數(若是有的話)進行求值以後(在函數主體中執行任何表達式或語句以前)在函數調用(函數是否爲內聯)時(第§1.9/17 )。

1:注意:對全表達式的評估能夠包括對不屬於全表達式的詞法部分的子表達式的評估。 例如,與評估默認參數表達式(8.3.6)有關的子表達式被認爲是在調用函數的表達式中建立的,而不是在定義默認參數的表達式中建立的

2:所指示的運算符是內置運算符,如第5節所述。在有效上下文中,若是其中一個運算符被重載(第13節),從而指定了用戶定義的運算符函數,則該表達式指定函數調用,而且操做數造成一個參數列表,它們之間沒有隱含的序列點。


什麼是未定義行爲?

該標準在第§1.3.12§1.3.12未定義行爲定義爲

行爲,例如在使用錯誤的程序構造或錯誤的數據時可能發生的行爲,對此本國際標準不施加任何要求3

當本國際標準省略對行爲的任何明肯定義的描述時,也可能會出現未定義的行爲。

3:容許的不肯定行爲,範圍從徹底忽略具備不可預測結果的狀況,到在翻譯或程序執行期間以環境特徵的書面方式記錄的行爲(有無診斷消息),到終止翻譯或執行(發出診斷消息)。

簡而言之,未定義的行爲意味着從守護程序從鼻子飛出到女朋友懷孕可能發生任何事情。


未定義行爲和序列點之間有什麼關係?

在開始討論以前,您必須瞭解「 未定義行爲」,「未指定行爲」和「實現已定義行爲」之間的區別。

您還必須知道, the order of evaluation of operands of individual operators and subexpressions of individual expressions, and the order in which side effects take place, is unspecified

例如:

int x = 5, y = 6;

int z = x++ + y++; //it is unspecified whether x++ or y++ will be evaluated first.

這裏的另外一個例子。


如今,第§5/4的標準說

  • 1) 在上一個序列點與下一個序列點之間,標量對象最多應經過表達式的求值修改其存儲值。

這是什麼意思?

非正式地,它意味着兩個序列點之間的變量不得被屢次修改。 在表達式語句中, next sequence point一般在終止分號處,而上previous sequence point一條語句的末尾。 一個表達式也能夠包含中間sequence points

從上面的句子中,如下表達式調用未定義的行爲:

i++ * ++i;   // UB, i is modified more than once btw two SPs
i = ++i;     // UB, same as above
++i = 2;     // UB, same as above
i = ++i + 1; // UB, same as above
++++++i;     // UB, parsed as (++(++(++i)))

i = (i, ++i, ++i); // UB, there's no SP between `++i` (right most) and assignment to `i` (`i` is modified more than once btw two SPs)

可是如下表達式很好:

i = (i, ++i, 1) + 1; // well defined (AFAIK)
i = (++i, i++, i);   // well defined 
int j = i;
j = (++i, i++, j*i); // well defined

  • 2) 此外,應僅訪問先驗值以肯定要存儲的值。

這是什麼意思? 這意味着,若是將對象寫入完整表達式內,則在同一表達式內對該對象的任何和全部訪問都必須直接參與要寫入的值的計算

例如,在i = i + 1i全部訪問(在LHS和RHS中)都直接涉及要寫入的值的計算 。 很好。

該規則將法律表達方式有效地限制在那些在修改以前明顯能夠訪問的表達方式。

範例1:

std::printf("%d %d", i,++i); // invokes Undefined Behaviour because of Rule no 2

範例2:

a[i] = i++ // or a[++i] = i or a[i++] = ++i etc

之因此被禁止,是由於i的訪問之一( a[i]中的訪問)與最終存儲在i中的值(在i++發生)無關,所以沒有很好的方法來定義-不管是出於咱們的理解仍是編譯器的理解-訪問是在存儲增量值以前仍是以後進行。 所以,行爲是不肯定的。

例子3:

int x = i + i++ ;// Similar to above

在此處跟蹤C ++ 11的答案。


#2樓

這是我以前的回答的後續文章,其中包含與C ++ 11相關的材料。


先決條件 :關係(數學)基礎知識。


C ++ 11中沒有序列點是真的嗎?

是! 這是真的。

在C ++ 11中, 序列點已被以前順序以後的順序 (以及未排序不肯定地排序關係取代。


「先排序」究竟是什麼東西?

排序前 (第1.9 / 13節)是一個關係:

單個線程執行的評估之間,並得出嚴格的偏序 1

正式它意味着給定任意兩個評價(見下文) AB ,若是A以前測序 B ,而後執行A 應當先於執行B 。 若是A是否是以前測序BB沒有以前測序A ,而後AB未測序 2。

AB以前先測序或BA以前先測序時,評估AB順序不肯定 ,但不肯定哪3

[筆記]
1:嚴格的偏序是在asymmetric且可transitive的集合P二進制關係 "<" ,即對於P全部abc ,咱們都有:
........(一世)。 若是a <b而後¬(b <a)( asymmetry );
........(ii)。 若是a <b和b <c,則a <c( transitivity )。
2: 無序評估的執行可能會重疊
3: 不肯定順序的求值不能重疊 ,但能夠先執行。


在C ++ 11的上下文中,「評估」一詞的含義是什麼?

在C ++ 11中,對錶達式(或子表達式)的求值一般包括:

  • 值計算 (包括肯定對象的身份以進行glvalue評估和獲取先前分配給對象的值以進行prvalue評估 )和

  • 引起反作用

如今(第1.9 / 14節)說:

要評估下一個完整表達式關聯的每一個值計算和反作用以前,對與一個完整表達式關聯的每一個值計算和反作用進行排序

  • 瑣碎的例子:

    int x; x = 10; ++x;

    x = 10;的值計算和反作用以後,對與++x相關的值計算和反作用進行排序x = 10;


所以,未定義行爲與上述事物之間必須存在某種關係,對嗎?

是! 對。

在(§1.9/ 15)中提到

除非另有說明,不然對單個運算符的操做數和單個表達式的子表達式的求值是無序列的 4

例如 :

int main()
{
     int num = 19 ;
     num = (num << 3) + (num >> 3);
}
  1. +運算符的操做數的求值相對於彼此是無序的。
  2. <<>>運算符的操做數的求值相對於彼此是無序列的。

4:在做爲程序的執行期間不止一次計算的表達式, 未測序不定測序其子表達式的評估不須要在不一樣的評價一致的方式進行。

(第1.9 / 15節)運算符的操做數的值計算在運算符結果的值計算以前排序。

這意味着在x + y ,對xy的值計算在(x + y)的值計算以前進行排序。

更重要的是

(第1.9 / 15節)若是相對於任何一個標量對象的反作用未排序

(a) 對同一標量對象的另外一種反作用

要麼

(b) 使用相同標量對象的值進行值計算。

行爲是不肯定的

例子:

int i = 5, v[10] = { };
void  f(int,  int);
  1. i = i++ * ++i; // Undefined Behaviour
  2. i = ++i + i++; // Undefined Behaviour
  3. i = ++i + ++i; // Undefined Behaviour
  4. i = v[i++]; // Undefined Behaviour
  5. i = v[++i]: // Well-defined Behavior
  6. i = i++ + 1; // Undefined Behaviour
  7. i = ++i + 1; // Well-defined Behaviour
  8. ++++i; // Well-defined Behaviour
  9. f(i = -1, i = -1); // Undefined Behaviour (see below)

在調用一個函數時(不管該函數是否爲內聯),與任何參數表達式或指定所調用函數的後綴表達式相關聯的每一個值計算和反作用都將在執行主體中的每一個表達式或語句以前進行排序。稱爲函數。 [ 注意: 與不一樣參數表達式關聯的值計算和反作用是無序列的 。 — 尾註 ]

表達式(5)(7)(8)不會調用未定義的行爲。 請查看如下答案以得到更詳細的說明。


最後說明

若是您發現帖子中有任何缺陷,請發表評論。 高級用戶(表明> 20000)請隨時編輯帖子以更正錯別字和其餘錯誤。


#3樓

我猜這是發生這種變化的根本緣由,這不只是使舊的解釋更清晰的表象:緣由是併發。 未指定的詳細說明順序僅是從幾種可能的順序中選擇一種,這與順序以前和以後都大不相同,由於若是沒有指定的順序,則能夠進行併發求值:舊規則則不行。 例如在:

f (a,b)

先前是a而後b,或者b而後a。 如今,可使用交錯的指令甚至在不一樣的內核上評估a和b。


#4樓

C ++ 17N4659 )包含一項提議, 用於 N4659 C ++的 表達式評估順序,該提案定義了更嚴格的表達式評估順序。

特別是,增長了如下句子

8.18賦值和複合賦值運算符
....

在全部狀況下,賦值都在左右操做數的值計算以後和賦值表達式的值計算以前進行排序。 右操做數在左操做數以前排序。

它使之前未定義的行爲的幾種狀況有效,包括所討論的一種狀況:

a[++i] = i;

可是,其餘幾種相似的狀況仍然會致使不肯定的行爲。

N4140

i = i++ + 1; // the behavior is undefined

可是在N4659

i = i++ + 1; // the value of i is incremented
i = i++ + i; // the behavior is undefined

固然,使用兼容C ++ 17的編譯器並不必定意味着應該開始編寫這樣的表達式。


#5樓

到目前爲止,在該討論中彷佛缺乏的C99(ISO/IEC 9899:TC3) ,針對評估順序進行了如下修改。

子表達式的評估順序和發生反作用的順序均未指定。 (第6.5頁67)

未指定操做數的評估順序。 若是試圖修改賦值運算符的結果或在下一個序列點以後訪問它,則行爲[sic]未定義。(第6.5.16頁的第91頁)

相關文章
相關標籤/搜索