C++17 added a useful extension to the if-statement syntax, by making it possible to include initialisation inside the if-statement. This is called “if statement with initializer”. You can use it like this:
if (int spuds = getNumberOfSpuds()) {
bakeSpuds(spuds);
}
Providing spuds != 0 it will obligingly bake them for us (in fact, it will even bake negative spuds… very handy in the current financial climate). Better yet, you can also add a condition, like this:
if (int spuds = getNumberOfSpuds(); spuds > 10) {
std::cout << "we have plenty of spuds" << std::endl;
}
This is often used to check pointers before dereferencing them:
if (auto* car = getCar()) {
car->setHandbrake(true);
car->setSteeringAngle(70.f);
}
Obviously these are very useful features, as it brings the variable instantiation into the scope of the if-statement and makes for neater code. We love it and use it a lot!
Unfortunately, this also seems to have led to a common misconception in how the condition can be used. In working on some fairly large C++ projects, we found quite a few instances of this type of code:
if (auto* car = getCar(); car->hasWheels()) {
// DANGER: if car is nullptr, will dereference nullptr
}
The intent of this code is obvious: the developer clearly expected it to only execute the condition if car != nullptr. Essentially, they expected it to check that car != nullptr, then execute the condition, and only enter the if-statement if car is valid, and the car has wheels. However, it doesn’t work like that. It will always execute the condition and will dereference the car pointer, regardless of whether it is nullptr or not, so this code is quite likely to crash.
Usually this is fairly easily fixed by doing something like this:
if (auto* car = getCar(); car && car->hasWheels()) {
std::cout << "we have a car, and it has wheels" << std::endl;
}
But the really nasty thing about buggy code like this is that it can often appear to work fine, until one day it blows up. This can lead to really nasty intermittent bugs, which are exceedingly hard to debug. Worse still, static code analysis tools might not spot this issue, and we had obviously missed some of these in code reviews also. If you suspect bugs like this are present, it’s also quite hard to grep your code base to find them. For example: searching for lines containing “if” and a semicolon will bring up a huge number of hits, which have to be searched manually. This makes it especially important not to introduce them in the first place đ
Here’s an example of some such code which appears to work (at least, it doesn’t crash), but is obviously not correct:
struct Car {
bool hasWheels() {
return true;
}
};
Car* nullCar = nullptr;
if (auto* car = nullCar) {
std::cout << "we have a car" << std::endl;
}
if (auto* car = nullCar; car->hasWheels()) {
std::cout << "car has wheels" << std::endl;
// Spoiler: this may be a lie
}
If you run this, it won’t output “we have a car”, but it probably will output “car has wheels”, even though car is nullptr. It does that because hasWheels is just returning a constant value, so the compiler doesn’t actually need to use our pointer. Although this is a somewhat contrived example (because our function could have been static, and we are using raw pointers etc), I hope it illustrates the point: that just because code appears to work when you test it, that doesn’t necessarily prove the code is correct.
Another misconception I’ve seen in practice is when people put initialisation in both the initialisation and condition, assuming that the compiler will check both conditions (it doesn’t):
if (auto* foo = getFoo(); auto* bar = getBar()) {
// DANGER: if foo == nullptr and bar != nullptr
}
It will enter the if statement if bar != nullptr, even if foo == nullptr. For example, this code would result in a nullptr dereference, because it only cares whether the condition on the right hand side is true, and doesn’t care whether car is nullptr or not:
if (auto* car = nullptr; auto* driver = new Driver) {
// DANGER: if car == nullptr, it will dereference the nullptr
car->setDriver(driver);
}
It’s worth saying that this code wasn’t introduced by inexperienced C++ developers. In fact, some of it was written by very capable, experienced and knowledgeable C++ developers, some with many years of experience. It just goes to show how easy it is to slip up and introduce this kind of bug. I suspect this is a fairly common issue, and something that it is useful to be aware of.
Finally, if you want to search for this kind of issue in a large code base, it is somewhat possible by using regular expressions. For example, in Visual Studio’s “Find in Files” option, you can select “Use regular expressions”. To find lines that contain both “if” and a semicolon you can do something like this:
if.*;
This expression should find lines that contain “if” and a semicolon, but the semicolon can’t be at the end of the line:
\bif\b.*;(?!\s*$)
Don’t ask me about regular expressions, because I am no expert – just sharing one that worked for me đ
Either way, this will bring up a fairly long list of search results that you will have to sift through manually. All the more reason to take care not to introduce them in the first place…