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