C/C++中的序列點(詳解)

轉自http://www.blogjava.net/zellux/archive/2008/05/16/200811.html 

發信人: NetMD (C++), 信區: CPlusPlus
標  題: [FAQ] C/C++中的序列點
發信站: 水木社區 (Wed Feb  7 01:13:41 2007), 站內

C/C++中的序列點


0. 什麼是副作用(side effects)

C99定義如下
Accessing a volatile object, modifying an object, modifying a file, or 
calling a function that does any of those operations are all side effects, 
which are changes in the state of the execution environment.

C++2003定義如下
Accessing an object designated by a volatile lvalue, modifying an object, 
calling a library I/O function, or calling a function that does any of 
those operations are all side effects, which are changes in the state of 
the execution environment.

可以看出C99和C++2003對副作用的定義基本類似,一個程序可以看作一個狀態機,在
任意一個時刻程序的狀態包含了它的所有對象內容以及它的所有文件內容(標準輸入
輸出也是文件),副作用會導致狀態的跳轉

一個變量一旦被聲明爲volatile-qualified類型,則表示該變量的值可能會被程序之
外的事件改變,每次讀取出來的值只在讀取那一刻有效,之後如果再用到該變量的值
必須重新讀取,不能沿用上一次的值,因此讀取volatile-qualified類型的變量也被
認爲是有副作用,而不僅僅是改寫

注,一般不認爲程序的狀態包含了CPU寄存器的內容,除非該寄存器代表了一個變量,
例如
void foo() {
  register int i = 0;  // 變量i被直接放入寄存器中,本文中被稱爲寄存器變量
                       // 注,register只是一個建議,不一定確實放入寄存器中
                       // 而且沒有register關鍵字的auto變量也可能放入寄存器
                       // 這裏只是用來示例,假設i確實放入了寄存器中
  i = 1;  // 寄存器內容改變,對應了程序狀態的改變,該語句有副作用
  i + 1;  // 編譯時該語句一般有警告:“warning: expression has no effect”
          // CPU如果執行這個語句,也肯定會改變某個寄存器的值,但是程序狀態
          // 並未改變,除了代表i的寄存器,程序狀態不包含其他寄存器的內容,
          // 因此該語句沒有任何副作用
}
特別的,C99和C++2003都指出,no effect的expression允許不被執行
An actual implementation need not evaluate part of an expression if it 
can deduce that its value is not used and that no needed side effects 
are produced (including any caused by calling a function or accessing 
a volatile object).


1. 什麼是序列點(sequence points)

C99和C++2003對序列點的定義相同
At certain specified points in the execution sequence called sequence 
points, all side effects of previous evaluations shall be complete and 
no side effects of subsequent evaluations shall have taken place.

中文表述爲,序列點是一些被特別規定的位置,要求在該位置前的evaluations所
包含的一切副作用在此處均已完成,而在該位置之後的evaluations所包含的任何
副作用都還沒有開始

例如C/C++都規定完整表達式(full-expression)後有一個序列點
extern int i, j;
i = 0;
j = i;
上面的代碼中i = 0以及j = i都是一個完整表達式,;說明了表達式的結束,因此
在;處有一個序列點,按照序列點的定義,要求在i = 0之後j = i之前的那個序列
點上對i = 0的求值以及副作用全部結束(0被寫入i中),而j = i的任何副作用都
還沒有開始。由於j = i的副作用是把i的值賦給j,而i = 0的副作用是把i賦值爲
0,如果i = 0的副作用發生在j = i之後,就會導致賦值後j的值是i的舊值,這顯
然是不對的

由序列點以及副作用的定義很容易看出,在一個序列點上,所有可能影響程序狀態
的動作均已完成,那這樣能否推斷出在一個序列點上一個程序的狀態應該是確定的
呢?!答案是不一定,這取決於我們代碼的寫法。但是,如果在一個序列點上程序
的狀態不能被確定,那麼標準規定這樣的程序是undefined behavior,稍後會解釋
這個問題


2. 表達式求值(evaluation of expressions)與副作用發生的相互順序

C99和C++2003都規定
Except where noted, 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.

也就是說,C/C++都指出一般情況下在表達式求值過程中的操作數求值順序以及副
作用發生順序是未說明的(unspecified)。爲什麼C/C++不詳細定義這些順序呢?
原因是因爲C/C++都是極端追求效率的語言,不規定這些順序,是爲了允許編譯器
有更大的優化餘地,例如
extern int *p;
extern int i;
*p = i++;  // (1)
根據前述規定,在表達式(1)中到底是*p先被求值還是i++先被求值是由編譯器決定
的;兩次副作用(對*p賦值以及i++)發生的順序是由編譯器決定的;甚至連子表
達式i++的求值(就是初始時i的值)以及副作用(將i增加1)都不需要同步發生,
編譯器可以先用初始時i的值(即子表達式i++的值)對*p賦值,然後再將i增加1,
這樣就把子表達式i++的整個計算過程分成了兩個不相鄰的步驟。而且通常編譯器
都是這麼實現的,原因在於i++的求值過程同*p = i++是有區別的,對於單獨的表
達式i++,執行順序一般是(假設不考慮inc指令):先將i加載到某個寄存器A(如
果i是寄存器變量則此步驟可以跳過)、將寄存器A的值加1、將寄存器A的新值寫回
i的地址;對於*p = i++,如果要先完整的計算子表達式i++,由於i++表達式的值
是i的舊值,因此還需要一個額外的寄存器B以及一條額外的指令來輔助*p = i++的
執行,但是如果我們先將加載到A的值寫回到*p,然後再執行對i增加1的指令,則
只需要一個寄存器即可,這種做法在很多平臺都有重要意義,因爲寄存器的數目往
往是有限的,特別是假如有人寫出如下的語句
extern int i, j, k, x;
x = (i++) + (j++) + (k++);
編譯器可以先計算(i++) + (j++) + (k++)的值,然後再對i、j、k各自加1,最後
將i、j、k、x寫回內存,這比每次完整的執行完++語義效率要高


3. 序列點對副作用的限制

C99和C++2003都有類似的如下規定
Between the previous and next sequence point a scalar object shall 
have its stored value modified at most once by the evaluation of an 
expression. Furthermore, the prior value shall be accessed only to 
determine the value to be stored. The requirements of this paragraph 
shall be met for each allowable ordering of the subexpressions of a 
full expression; otherwise the behavior is undefined.

也就是說,在相鄰的兩個序列點之間,一個對象只允許被修改一次,而且如果一個
對象被修改則在這兩個序列點之間對該變量的讀取的唯一目的只能是爲了確定該對
象的新值(例如i++,需要先讀取i的值以確定i的新值是舊值+1)。特別的,標準
要求任意可能的執行順序都必須滿足該條件,否則代碼將是undefined behavior

之所以序列點會對副作用有如此的限制,就是因爲C/C++標準沒有規定子表達式求
值以及副作用發生之間的順序,例如
extern int i, a[];
extern int foo(int, int);
i = ++i + 1;  // 該表達式對i所做的兩次修改都需要寫回對象,i的最終值取決
              // 於到底哪次寫回最後發生,如果賦值動作最後寫回,則i的值
              // 是i的舊值加2,如果++i動作最後寫回,則i的值是舊值加1,
              // 因此該表達式的行爲是undefined
a[i++] = i;  // 如果=左邊的表達式先求值並且i++的副作用被完成,則右邊的
             // 值是i的舊值加1,如果i++的副作用最後完成,則右邊的值是i
             // 的舊值,這也導致了不確定的結果,因此該表達式的行爲將是
             // undefined
foo(foo(0, i++), i++);  // 對於函數調用而言,標準沒有規定函數參數的求值
                        // 順序,但是標準規定所有參數求值完畢進入函數體
                        // 執行之前有一個序列點,因此這個表達式有兩種執
                        // 行方式,一種是先求值外層foo調用的i++然後求值
                        // foo(0, i++),然後進入到foo(0, i++)執行,這之
                        // 前有個序列點,這種執行方式還是在兩個相鄰序列
                        // 點之間修改了i兩次,undefined
                        // 另一種執行方式是先求值foo(0, i++),由於這裏
                        // 有一個序列點,隨後的第二個i++求值是在新序列
                        // 點之後,因此不算是兩個相鄰的序列點之間修改i
                        // 兩次
                        // 但是,前面已經指出標準規定任意可能的執行路徑
                        // 都必須滿足條件纔是定義好的行爲,這種代碼仍然
                        // 是undefined

前面我提到在一個序列點上程序的狀態不一定是確定的,原因就在於相鄰的兩個序
列點之間可能會發生多個副作用,這些副作用的發生順序是未指定的,如果多於一
個的副作用用於修改同一個對象,例如示例代碼i = ++i + 1;,則程序的結果是依
賴於副作用發生順序的;另外,如果某個表達式既修改了某個對象又需要讀取該對
象的值,且讀取對象的值並不用於確定對象新值,則讀取和修改兩個動作的先後順
序也會導致程序的狀態不能唯一確定
所幸的是,“在相鄰的兩個序列點之間,一個對象只允許被修改一次,而且如果一
個對象被修改則在這兩個序列點之間只能爲了確定該對象的新值而讀一次”這一強
制規定保證了符合要求的程序在任何一個序列點位置上其狀態都可以確定下來

注,由於對於UDT類型存在operator重載,函數語義會提供新的序列點,因此某些
對於built-in類型是undefined behavior的表達式對於UDT確可能是良好定義的,
例如
i = i++;  // 如果i是built-in類型對象,則該表達式在兩個相鄰的序列點之間對
          // i修改了兩次,undefined
          // 如果i是UDT類型該表達式也許是i.operator=(i.operator++(int)),
          // 函數參數求值完畢後會有一個序列點,因此該表達式並沒有在兩個
          // 相鄰的序列點之間修改i兩次,OK

由此可見,常見的問題如printf("%d, %d", i++, i++)這種寫法是錯誤的,這類問
題作爲筆試題或者面試題是沒有任何意義的
類似的問題同樣發生在cout << i++ << i++這種寫法上,如果overload resolution
選擇成員函數operator<<,則等價於(cout.operator<<(i++)).operator<<(i++),
否則等價於operator<<(operator<<(cout, i++), i++),如果i是built-in類型對
象,這種寫法跟foo(foo(0, i++), i++)的問題一致,都是未定義行爲,因爲存在
某條執行路徑使得i會在兩個相鄰的序列點之間被修改兩次;如果i是UDT則該寫法
是良好定義的,跟i = i++一樣,但是這種寫法也是不推薦的,因爲標準對於函數
參數的求值順序是unspecified,因此哪個i++先計算是不能預計的,這仍舊會帶來
移植性的問題,這種寫法應該避免


4. 編譯器的跨序列點優化

根據前述討論可知,在同一個表達式內對於同一個變量i,允許的行爲是
A. 不讀取,改寫一次,例如
     i = 0;
B. 讀取一次或者多次,改寫一次,但所有讀取僅僅用於決定改寫後的新值,例如
     i = i + 1;  // 讀取一次,改寫一次
     i = i & (i - 1);  // 讀取兩次,改寫一次,感謝puke給出的例子
C. 不改寫,讀取一次或者多次,例如
     j = i & (i - 1);

對於情況B和C,編譯器是有一定的優化權利的,它可以只讀取一次變量的值然後
直接使用該值多次

但是,當該變量是volatile-qualified類型時編譯器允許的行爲究竟如何目前還
沒有找到明確的答案,ctrlz認爲如果在兩個相鄰序列點之間讀取同一個volatile-
qualified類型對象多次仍舊是undefined behavior,原因在於該讀取動作有副作
用且該副作用等價於修改該對象,RoachCock的意見是兩個相鄰的序列點之間讀取
同一個volatile-qualified類型應該是合法的,但是不能被優化成只讀一次。一
段在嵌入式開發中很常見的代碼示例如下
extern volatile int i;
if (i != i) {  // 探測很短的時間內i是否發生了變化
  // ...
}
如果i != i被優化爲只讀一次,則結果恆爲false,故RoachCock認爲編譯器不能
夠對volatile-qualified類型的變量做出只讀一次的優化。ctrlz則認爲這段代碼
本身是不正確的,應該改寫成
int j = i;
if (j != i) {  // 將對volatile-qualified類型變量的多次讀取用序列點隔開
  // ...
}

雖然尚不能確定volatile-qualified類型的變量在相鄰兩個序列點之間讀取多次
行爲是否合法以及將如何優化(不管怎麼樣,對於volatile-qualified類型這種
代碼應該儘量避免),但是可以肯定的是,對於volatile-qualified類型的變量
在跨序列點之後必須要重新讀取,volatile就是用來阻止編譯器做出跨序列點的
過激優化的,而對於non-volatile-qualified類型的跨序列點多次讀取則可能被
優化成只讀一次(直到某個語句或者函數對該變量發生了修改,在此之前編譯器
可以假定non-volatile-qualified類型的變量是不會變化的,因爲目前的C/C++
抽象機器模型是單線程的),例如
bool flag = true;
void foo() {
  while (flag) {  // (2)
    // ...
  }
}
如果編譯器探測到foo()沒有任何語句(包括foo()調用過的函數)對flag有過修
改,則也許會把(2)優化成只在進入foo()的時候讀一次flag的值而不是每次循環
都讀一次,這種跨序列點的優化很有可能導致死循環。但是這種代碼在多線程編
程中很常見,雖然foo()沒有修改過flag,也許在另一個線程的某個函數調用中
會修改flag以終止循環,爲了避免這種跨序列點優化帶來到錯誤,應該把flag聲
明爲volatile bool,C++2003對volatile的說明如下
[Note: volatile is a hint to the implementation to avoid aggressive 
optimization involving the object because the value of the object 
might be changed by means undetectable by an implementation. See 1.9 
for detailed semantics. In general, the semantics of volatile are 
intended to be the same in C++ as they are in C. ]


5. C99定義的序列點列表

— The call to a function, after the arguments have been evaluated.
— The end of the first operand of the following operators: 
     logical AND && ; 
     logical OR || ; 
     conditional ? ; 
     comma , .
— The end of a full declarator: 
     declarators;
— The end of a full expression: 
     an initializer; 
     the expression in an expression statement; 
     the controlling expression of a selection statement (if or switch); 
     the controlling expression of a while or do statement; 
     each of the expressions of a for statement; 
     the expression in a return statement.
— Immediately before a library function returns.
— After the actions associated with each formatted input/output function 
   conversion specifier.
— Immediately before and immediately after each call to a comparison 
   function, and also between any call to a comparison function and any 
   movement of the objects passed as arguments to that call.


6. C++2003定義的序列點列表

所有C99定義的序列點同樣是C++2003所定義的序列點
此外,C99只是規定庫函數返回之後有一個序列點,並沒有規定普通函數返回之後
有一個序列點,而C++2003則特別指出,進入函數(function-entry)和退出函數
(function-exit)各有一個序列點,即拷貝一個函數的返回值之後同樣存在一個
序列點

需要特別說明的是,由於operator||、operator&&以及operator,可以重載,當它
們使用函數語義的時候並不提供built-in operators所規定的那幾個序列點,而
僅僅只是在函數的所有參數求值後有一個序列點,此外函數語義也不支持||、&&
的短路語義,這些變化很有可能會導致難以發覺的錯誤,因此一般不建議重載這
幾個運算符


7. C++2003中兩處關於lvalue的修改對序列點的影響

在C語言中,assignment operators的結果是non-lvalue,C++2003則將assignment
operators的結果改成了lvalue,目前尚不清楚這一改動對於built-in類型有何意
義,但是它卻導致了很多在合法的C代碼在目前的C++中是undefined behavior,例

extern int i;
extern int j;
i = j = 1;
由於(j = 1)的結果是lvalue,該結果作爲給i賦值的右操作數,需要一個lvalue-
to-rvalue conversion,這個conversion代表了一個讀取語義,因此i = j = 1就
是先將1賦值給j,然後讀取j的值賦值給i,這個行爲是undefined,因爲標準規定
兩個相鄰序列點之間的讀取只能用於決定修改對象的新值,而不能發生在修改之後
再讀取
由於C++2003規定assignment operators的結果是lvalue,因此下列在C99中非法的
代碼在C++2003中卻是可以通過編譯的
extern int i;
(i += 1) += 2;
顯然按照C++2003的規定這個代碼的行爲是undefined,它在兩個相鄰的序列點之間
修改了i兩次

類似的問題同樣發生在built-in類型的前綴++/--operators上,C++2003將前綴++/--
的結果從rvalue修改爲lvalue,這甚至導致了下列代碼也是undefined behavior
extern int i;
extern int j;
i = ++j;
同樣是因爲lvalue作爲assignment operator的右操作數需要一個左值轉換,該轉
換導致了一個讀取動作且這個讀取動作發生在修改對象之後

C++的這一改動顯然是考慮不周的,導致了很多C語言的習慣寫法都成了undefined
behavior,因此Andrew Koenig在1999年的時候就向C++標準委員會提交了一個建
議要求爲assignment operators增加新的序列點,但是到目前爲止C++標準委員會
都還沒有就該問題達成一致意見,我將Andrew Koenig的提議附後,如果哪位有時
間有興趣,可以看看,不過不看也不會有任何損失 :-)


222. Sequence points and lvalue-returning operators 
Section: 5  expr     Status: drafting     Submitter: Andrew Koenig     Date: 20 Dec 1999

I believe that the committee has neglected to take into account one of the differences between C and C++ when defining sequence points. As an example, consider

    (a += b) += c;

where a, b, and c all have type int. I believe that this expression has undefined behavior, even though it is well-formed. It is not well-formed in C, because += returns an rvalue there. The reason for the undefined behavior is that it modifies the value of `a' twice between sequence points.

Expressions such as this one are sometimes genuinely useful. Of course, we could write this particular example as

    a += b; a += c;

but what about

    void scale(double* p, int n, double x, double y) {
        for (int i = 0; i < n; ++i) {
            (p[i] *= x) += y;
        }
    }

All of the potential rewrites involve multiply-evaluating p[i] or unobvious circumlocations like creating references to the array element.

One way to deal with this issue would be to include built-in operators in the rule that puts a sequence point between evaluating a function's arguments and evaluating the function itself. However, that might be overkill: I see no reason to require that in

    x[i++] = y;

the contents of `i' must be incremented before the assignment.

A less stringent alternative might be to say that when a built-in operator yields an lvalue, the implementation shall not subsequently change the value of that object as a consequence of that operator.

I find it hard to imagine an implementation that does not do this already. Am I wrong? Is there any implementation out there that does not `do the right thing' already for (a += b) += c?

5.17  expr.ass paragraph 1 says,

The result of the assignment operation is the value stored in the left operand after the assignment has taken place; the result is an lvalue. 
What is the normative effect of the words "after the assignment has taken place"? I think that phrase ought to mean that in addition to whatever constraints the rules about sequence points might impose on the implementation, assignment operators on built-in types have the additional constraint that they must store the left-hand side's new value before returning a reference to that object as their result.

One could argue that as the C++ standard currently stands, the effect of x = y = 0; is undefined. The reason is that it both fetches and stores the value of y, and does not fetch the value of y in order to compute its new value.

I'm suggesting that the phrase "after the assignment has taken place" should be read as constraining the implementation to set y to 0 before yielding the value of y as the result of the subexpression y = 0.

Note that this suggestion is different from asking that there be a sequence point after evaluation of an assignment. In particular, I am not suggesting that an order constraint be imposed on any side effects other than the assignment itself. 
Francis Glassborow:

My understanding is that for a single variable:

Multiple read accesses without a write are OK 
A single read access followed by a single write (of a value dependant on the read, so that the read MUST happen first) is OK 
A write followed by an actual read is undefined behaviour 
Multiple writes have undefined behaviour 
It is the 3) that is often ignored because in practice the compiler hardly ever codes for the read because it already has that value but in complicated evaluations with a shortage of registers, that is not always the case. Without getting too close to the hardware, I think we both know that a read too close to a write can be problematical on some hardware.

So, in x = y = 0;, the implementation must NOT fetch a value from y, instead it has to "know" what that value will be (easy because it has just computed that in order to know what it must, at some time, store in y). From this I deduce that computing the lvalue (to know where to store) and the rvalue to know what is stored are two entirely independent actions that can occur in any order commensurate with the overall requirements that both operands for an operator be evaluated before the operator is.

Erwin Unruh:

C distinguishes between the resulting value of an assignment and putting the value in store. So in C a compiler might implement the statement x=y=0; either as x=0;y=0; or as y=0;x=0; In C the statement (x += 5) += 7; is not allowed because the first += yields an rvalue which is not allowed as left operand to +=. So in C an assignment is not a sequence of write/read because the result is not really "read".

In C++ we decided to make the result of assignment an lvalue. In this case we do not have the option to specify the "value" of the result. That is just the variable itself (or its address in a different view). So in C++, strictly speaking, the statement x=y=0; must be implemented as y=0;x=y; which makes a big difference if y is declared volatile.

Furthermore, I think undefined behaviour should not be the result of a single mentioning of a variable within an expression. So the statement (x +=5) += 7; should NOT have undefined behaviour.

In my view the semantics could be:

if the result of an assignment is used as an rvalue, its value is that of the variable after assignment. The actual store takes place before the next sequence point, but may be before the value is used. This is consistent with C usage. 
if the result of an assignment is used as an lvalue to store another value, then the new value will be stored in the variable before the next sequence point. It is unspecified whether the first assigned value is stored intermediately. 
if the result of an assignment is used as an lvalue to take an address, that address is given (it doesn't change). The actual store of the new value takes place before the next sequence point. 
Jerry Schwarz:

My recollection is different from Erwin's. I am confident that the intention when we decided to make assignments lvalues was not to change the semantics of evaluation of assignments. The semantics was supposed to remain the same as C's.

Ervin seems to assume that because assignments are lvalues, an assignment's value must be determined by a read of the location. But that was definitely not our intention. As he notes this has a significant impact on the semantics of assignment to a volatile variable. If Erwin's interpretation were correct we would have no way to write a volatile variable without also reading it. 

Lawrence Crowl:

For x=y=0, lvalue semantics implies an lvalue to rvalue conversion on the result of y=0, which in turn implies a read. If y is volatile, lvalue semantics implies both a read and a write on y.

The standard apparently doesn't state whether there is a value dependence of the lvalue result on the completion of the assignment. Such a statement in the standard would solve the non-volatile C compatibility issue, and would be consistent with a user-implemented operator=.

Another possible approach is to state that primitive assignment operators have two results, an lvalue and a corresponding "after-store" rvalue. The rvalue result would be used when an rvalue is required, while the lvalue result would be used when an lvalue is required. However, this semantics is unsupportable for user-defined assignment operators, or at least inconsistent with all implementations that I know of. I would not enjoy trying to write such two-faced semantics.

Erwin Unruh:

The intent was for assignments to behave the same as in C. Unfortunately the change of the result to lvalue did not keep that. An "lvalue of type int" has no "int" value! So there is a difference between intent and the standard's wording.

So we have one of several choices:

live with the incompatibility (and the problems it has for volatile variables) 
make the result of assignment an rvalue (only builtin-assignment, maybe only for builtin types), which makes some presently valid programs invalid 
introduce "two-face semantics" for builtin assignments, and clarify the sequence problematics 
make a special rule for assignment to a volatile lvalue of builtin type 
I think the last one has the least impact on existing programs, but it is an ugly solution.

Andrew Koenig:

Whatever we may have intended, I do not think that there is any clean way of making 

    volatile int v;
    int i;

    i = v = 42;

have the same semantics in C++ as it does in C. Like it or not, the subexpression v = 42 has the type ``reference to volatile int,'' so if this statement has any meaning at all, the meaning must be to store 42 in v and then fetch the value of v to assign it to i.

Indeed, if v is volatile, I cannot imagine a conscientious programmer writing a statement such as this one. Instead, I would expect to see 

    v = 42;
    i = v;

if the intent is to store 42 in v and then fetch the (possibly changed) value of v, or 
    v = 42;
    i = 42;

if the intent is to store 42 in both v and i.

What I do want is to ensure that expressions such as ``i = v = 42'' have well-defined semantics, as well as expressions such as (i = v) = 42 or, more realistically, (i += v) += 42 .

I wonder if the following resolution is sufficient:

Append to 5.17  expr.ass paragraph 1:

There is a sequence point between assigning the new value to the left operand and yielding the result of the assignment expression. 
I believe that this proposal achieves my desired effect of not constraining when j is incremented in x[j++] = y, because I don't think there is a constraint on the relative order of incrementing j and executing the assignment. However, I do think it allows expressions such as (i += v) += 42, although with different semantics from C if v is volatile.

Notes on 10/01 meeting: 

There was agreement that adding a sequence point is probably the right solution.

Notes from the 4/02 meeting: 

The working group reaffirmed the sequence-point solution, but we will look for any counter-examples where efficiency would be harmed.

For drafting, we note that ++x is defined in 5.3.2  expr.pre.incr as equivalent to x+=1 and is therefore affected by this change. x++ is not affected. Also, we should update any list of all sequence points.

Notes from October 2004 meeting: 

Discussion centered around whether a sequence point “between assigning the new value to the left operand and yielding the result of the expression” would require completion of all side effects of the operand expressions before the value of the assignment expression was used in another expression. The consensus opinion was that it would, that this is the definition of a sequence point. Jason Merrill pointed out that adding a sequence point after the assignment is essentially the same as rewriting

    b += a

as

    b += a, b

Clark Nelson expressed a desire for something like a “weak” sequence point that would force the assignment to occur but that would leave the side effects of the operands unconstrained. In support of this position, he cited the following expression:

    j = (i = j++)

With the proposed addition of a full sequence point after the assignment to i, the net effect is no change to j. However, both g++ and MSVC++ behave differently: if the previous value of j is 5, the value of the expression is 5 but j gets the value 6.

Clark Nelson will investigate alternative approaches and report back to the working group.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章