I’ve switch jobs and im working with C++, Objective-C++ and alot of cross platform stuff. Which is a nice change of pace from Java. I’ve extracted out the C++ unit testing framework from one of our IP projects and am actively using it to write unit test for a data exporter im working with, which brings me to my point for this post. And thanks to Kevin who helped me figure this out.
In C++, it turns out function-call arguments are not guaranteed by the standard to be evaluated in the traditional “left-to-right” order. Instead, it’s totaly implementation defined, and I’ve found that on VC++ (2k5) its as you would expect about 90% of the time, but there are certain situations where the compiler decides to mix things up.
the problem was noted in this piece of code:
Vec3 boundsMin( reader.readF32(), reader.readF32(), reader.readF32() )
the compiler translated it into something along these lines:
call readF32 /* stores the result in ecx */
push ecx
fstp dword ptr [esp]
call readF32
push ecx
fstp dword ptr [esp]
call readF32
push ecx
fstp dword ptr [esp]
lea [boundsMin]
call Vec3::Vec3
notice how the store instruction “fstp” always writes to the memory location specified by esp? now the assembly generated for Vec3 constructor’s uses the “rep stos” instruction to “block copy” the contents of the location esp refers to. Initially, all this code looks like it happens how you would expect: “left-to-right” but the reason it doesnt work as expected is because the “push” instruction is decrementing the value in esp by 4 and writing the value of the operand to the top of the stack every time its called.
What does this mean? in simple terms, yes, the reads happen in the order A,B,C but the resulting values are written into the stack-frame effectively backwards C,B,A (I say effectively because the “rep stos” instruction reads from lower memory to higher memory, but C was written to lower memory (because it was written last, hence esp would be at its lowest value), B in higher memory after C and A in even higher memory after B). This is where the non-guaratee by the standard comes into play, the only reason the compiler does this is because there is no requirement to do otherwise. hence all the values for this particular vector were having their x and z values swapped consistently in “Debug” generated code.
Fun times.
Oh and the lesson from all this: don’t inline function argument-expressions which have dependent reads. e.g. reading from streams.
For more reading: http://www.embedded.com/story/OEG20020429S0037
and: http://compilers.iecc.com/comparch/article/95-07-089