Beware of Sequence Points

Consider the following C++ code:

void foo(int a, int b, int c) 
{ 
   std::cout << a << ' ' << b << ' ' << c << std::endl; 
}   

int main(int argc, char* argv[]) 
{ 
   int value = 1; 
   foo(value++, value++, value++);   

   return 0; 
}

That looks pretty straight-forward: there is a function that takes three integer arguments and prints them to the console. In main, it is called by incrementing a variable three times. You would expect that the output was 1 2 3. But surprise: it is 3 2 1 in a debug build and 1 1 1 in a release build (Visual Studio 2005 and Visual Studio 2008). Why? Because it writes multiple times to the same memory location between two sequence points, which is undefined behavior (and I know somebody that always stresses that undefined behavior is spelled W-R-O-N-G).

A sequence point is a point in the execution of the program where all side effects from previous evaluation have been performed and no side effects from subsequent evaluation have been performed. Between two consecutive sequence points an object’s value can be modified only once by an expression. The function call operator is such a sequence point, but the order of arguments evaluations is not specified (unlike Java or C# where is always performed from left to right). Thus, modifying a variable multiple times in calling a function introduces undefined behavior.

Using the /RTCu (Run-time error checks) compiler option in the debug build produces different machine code and order of evaluation.

At machine code level, the code for main in the debug build looks like this:

; 34   :    

	mov	DWORD PTR _value$[ebp], 1   

; 35   :    

	mov	eax, DWORD PTR _value$[ebp] 
	mov	DWORD PTR tv65[ebp], eax 
	mov	ecx, DWORD PTR _value$[ebp] 
	add	ecx, 1   

	mov	DWORD PTR _value$[ebp], ecx 
	mov	edx, DWORD PTR _value$[ebp] 
	mov	DWORD PTR tv68[ebp], edx 
	mov	eax, DWORD PTR _value$[ebp] 
	add	eax, 1   

	mov	DWORD PTR _value$[ebp], eax 
	mov	ecx, DWORD PTR _value$[ebp] 
	mov	DWORD PTR tv71[ebp], ecx 
	mov	edx, DWORD PTR _value$[ebp] 
	add	edx, 1   

	mov	DWORD PTR _value$[ebp], edx 
	mov	eax, DWORD PTR tv65[ebp] 
	push	eax 
	mov	ecx, DWORD PTR tv68[ebp] 
	push	ecx 
	mov	edx, DWORD PTR tv71[ebp] 
	push	edx 
	call	?foo@@YAXHHH@Z				; foo 
	add	esp, 12					; 0000000cH

and in the release build (or without /RTCu):

; 34   :    

	mov	DWORD PTR _value$[ebp], 1   

; 35   :    

	mov	eax, DWORD PTR _value$[ebp] 
	mov	DWORD PTR tv65[ebp], eax   

	mov	ecx, DWORD PTR _value$[ebp] 
	mov	DWORD PTR tv68[ebp], ecx   

	mov	edx, DWORD PTR _value$[ebp] 
	mov	DWORD PTR tv71[ebp], edx   

	mov	eax, DWORD PTR tv65[ebp] 
	push	eax 
	mov	ecx, DWORD PTR tv68[ebp] 
	push	ecx 
	mov	edx, DWORD PTR tv71[ebp] 
	push	edx 
	call	?foo@@YAXHHH@Z				; foo 
	add	esp, 12					; 0000000cH   

	mov	eax, DWORD PTR _value$[ebp] 
	add	eax, 1 
	mov	DWORD PTR _value$[ebp], eax   

	mov	ecx, DWORD PTR _value$[ebp] 
	add	ecx, 1 
	mov	DWORD PTR _value$[ebp], ecx   

	mov	edx, DWORD PTR _value$[ebp] 
	add	edx, 1 
	mov	DWORD PTR _value$[ebp], edx

If you know a little bit assembly language you can see that in the first case value is incremented after each evaluation of foo’s arguments, and in the second case that happens only after the call to foo. After the call, in both cases, value becomes 4.

To achieve the intended behavior you should write:

int main(int argc, char* argv[]) 
{ 
   int value = 1; 
   foo(value, value+1, value+2);
   value += 3;

   return 0; 
}

It should be quite obvious, that the same behavior is encountered if the call to foo was replaced with:

std::cout << value++ << ' ' << value++ << ' ' << value++ << std::endl;

For more information about sequence points I suggest reading:
http://en.wikipedia.org/wiki/Sequence_point
http://c-faq.com/expr/seqpoints.html
http://msdn2.microsoft.com/en-us/library/d45c7a5d(VS.80).aspx

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.