My father, Nolan Capehart (also a Software Engineer) discovered this Visual Studio bug and wrote up an explanation.
Symptoms: You build a project in Visual Studio, set a breakpoint, and execute. However, the breakpoint immediately disappears (completely – not just temporarily disabled), and a breakpoint appears somewhere else. If execution stops on the new breakpoint, examination of the call stack might show that the function in which execution stopped was in fact called, or it might show that instead the function with the original breakpoint was called, or it might even show that a third function was called. If you go up one level in the call stack, and examine the disassembled code, you might see code to call the function in which execution stopped, or you might see code to call the function with the original breakpoint, or you might even see code to call a third function.
Explanation by Way of Demonstration:
Here’s a fun little demonstration of how to get yourself into trouble by optimizing a debug build, even when you don’t realize you’re optimizing. This demonstration works for me in Visual Studio 2008. There is possibly some element of randomness in some choices made by the compiler to cause this, so it might not work in other versions of VS, and it might not even work all the time in VS 2008.
Create a project. Make it a console project. Set these configuration settings for the debug build:
[C/C++].[General].[Debug Information Format] to “Program Database (/Zi)”.
[C/C++].[Code Generation].[Enable Function-Level Linking] to “Yes (/Gm)”.
[Linker].[Debugging].[Generate Map File] to “Yes (/MAP)”.
[Linker].[Optimization].[EnableCOMDAT Folding] = “Remove Redundant COMDATs (/OPT:ICF)”
Note that the problem will occur EVEN IF you disable optimization by setting [C/C++].[Optimization].[Optimization] to “Disabled (/Od)”.
Replace the contents of the main cpp file with this:
#include “stdafx.h”
void ambulate() {
}
class CJumper {
public:
CJumper() : m_NumberOfJumps(0) {}
void jump();
int m_NumberOfJumps;
} Jumper;
void CJumper::jump() {
ambulate();
++m_NumberOfJumps;
}
class CSkipper {
public:
CSkipper() : m_NumberOfSkips(0) {}
void skip();
int m_NumberOfSkips;
} Skipper;
void CSkipper::skip() {
ambulate();
++m_NumberOfSkips;
}
class CHopper {
public:
CHopper() : m_NumberOfHops(0) {}
void hop();
int m_NumberOfHops;
} Hopper;
void CHopper::hop() {
ambulate();
++m_NumberOfHops;
}
int _tmain(int argc, _TCHAR* argv[]) {
Skipper.skip();
return 0;
}
Put a breakpoint on any line of skip().
Build and execute the debug version.
Amazingly, the breakpoint on hop will disappear, and a breakpoint will magically appear on skip, and execution will stop there. But looking at the call stack or the disassembled code, you will see that jump() was called!!!
When COMDAT Folding is enabled, the linker will notice that each function has exactly the same code, so it will make only one function which will be called in all three instances. Unfortunately, this totally confuses the debugger.
Take a look at the addresses of hop, skip, and jump in the map file. They are all the same.
“But they DON’T all do the same thing,” I can hear you say. “One of them increments a member of CJumper, one increments a member of CSkipper, and the third increments a member of CHopper.”
‘Tis true, but what does that look like at the machine level? In each case, a call to the function consists of passing into the function a pointer (the “this” pointer), which the function will use as a pointer to an integer to be incremented. So they really ARE exactly the same.
Now insert:
int m_NotUsed;
immediately before the declaration of m_NumberOfSkips. Build and execute. This time, it works as expected, hitting the breakpoint in skip(). This happens because now skip is different than the other functions. Instead of incrementing the integer pointed to by its argument, it increments the next integer. So, the linker can no longer have one function to represent all three apparent functions.