Over the past hour or two I have been tracking down an especially frustrating bug. The bug in question was a Heisenbug that only appeared in the Release(WithDebugInfo) build, but not in the Debug build. It was solved by adding a single return true
statement to an otherwise empty function.
I happened across the bug when I was attempting to profile my game. The profiler is meant to be run with a Release build (so it gives accurate speed estimates with compiler optimizations), so I switched from a Debug build over to the RelWithDebugInfo build. However, when I did that, my program would crash whenever I used an attack in the game. Commenting out a line where the crash occurred that was trying to populate a HashMap with some new values, I could stop the crash, but in place of it the attack would simply never finish, the time within it never advancing beyond one frame.
This was a fairly frustrating bug, as debugging an optimized build isn't fun, as lots of things get optimized out, and sometimes run in a different order (at least, QtCreator/GDB makes it seem that the control is jumping around in the file and in different files). Switching back to a Debug build didn't help, as the bug would just disappear. Which left trying to figure out what was going on in the Release build, but it made no sense, as we seemed to have a segmentation fault on an object that existed on an outer call, but on an inner call it was suddenly trying to delete a nullptr
[^1] on a HashMap somewhere around address 0x80. That was the crash case, of course, and commenting out that line, for some reason the rest of the AttackState::UpdateSegment
seemed not to be executed, including the line that updates the time to add the time step for that frame.
The issue came down to the ApplyCost
function, copied below. Originally, I had just left it blank, leaving the compiler warning no return statement in function returning non-void
as a reminder to go back and implement the function later. This worked out fine on the Debug build, but it's actually Undefined Behavior. And for the Release build, the compiler does some optimizations based on this that end up breaking the game. If my assembly-foo were better, perhaps I could tell you what exactly it was, but knowledge of assembly has never been my strong point.
/// Applies an sp cost to the user. Returns true if the cost was applied, false if it could not be met.
bool ApplyCost(float cost)
{
//TODO: Actually apply the cost.
return true; // <-- Without this line the game breaks.
}
[1]: I briefly thought that the problem was actually caused by HashBase
(from HashMap
) deleting a nullptr
, but it turns out that is allowed. Specifically, the flow was
HashMap.h:L334 calls InsertNode when
ptrs_ == nullptr
HashMap.h:L664 callsAllocateBuckets(Size(), MIN_BUCKETS)
whenptrs_ == nullptr
HashBase.cpp:L32 deletesptrs_
always.