There’s an old expression developers use when someone makes a mistake. They say you “shot yourself in the foot.”
There’s a variation on the joke that describes the experience of shooting yourself in the foot in various programming languages. The descriptions have evolved, and some versions are funnier than others. But the C++ gag has remained the same since I first saw it, back when I was still wrestling with Rogue Wave Tools.h++, and the STL was only an unsubstantiated rumor. Here is one example:
You accidentally create a dozen instances of yourself and shoot them all in the foot. Providing emergency medical assistance is impossible since you can’t tell which are bitwise copies and which are just pointing at others and saying, “That’s me, over there.”
C++ lets you do just about anything. If you can convince the compiler that you’ve written legal code, it will compile it. Of course, this means that you can, well, shoot yourself in the foot. Let’s take a look at some of the pitfalls of C++.
We’ll define a pitfall as a bug that compiles but doesn’t do what you expect. There’s quite of a few of these bugs, but we’ll cover a handful.
Overriding Arguments in Virtual Functions
Let’s start with an example of a C++ pitfall with virtual functions.
Consider two classes. One is a subclass of the other.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
class Foo { public: virtual void doIt() { std::cout << "Doing foo" << std::endl; } }; class Bar : public Foo { public: virtual void doIt() { std::cout << "Doing bar" << std::endl; } }; |
Next, we have a mainfunction that accesses the subclass via a pointer to the base.
1 2 3 4 5 |
int main() { Bar bar; Foo* fooPtr = &bar; fooPtr->doIt(); } |
Now, when we run the program, we see this:
1 |
Doing bar |
That’s what we expect. When we access a subclass via a pointer to its base class, we expect the subclass’ version of a function to be executed.
But we can break this without even trying hard.
Now, let’s add a default argument to Bar’s implementation of doit().
1 2 3 4 5 6 7 8 |
class Bar : public Foo { public: virtual void doIt(int x = 0) { std::cout << "Doing bar" << std::endl; } }; |
Then, run the program again.
1 |
Doing foo |
Oops! C++ gave us the implementation of doit() we deserved, but not the one we needed. It ran Foo’s version of doIt() because it has no arguments.
This is a contrived example. Most developers wouldn’t overload a method and add a new default argument at the same time.
But, what if we don’t add a default argument, but change an existing one in a subclass?
First, let’s make a few changes to our two classes.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
class Foo { public: virtual void doIt(int x = 1) { std::cout << "Doing foo: " << x << std::endl; } }; class Bar : public Foo { public: virtual void doIt(int x = 0) { std::cout << "Doing bar " << x << std::endl; } }; |
Next, run this new version of our test program:
1 |
Doing bar: 1 |
We got the right method, but the wrong default value.
Well, we did get the right one because the compiler is always correct, even when it’s wrong.
Default parameters are trouble, and you’re best off avoiding them. But if you do need them, remember that they’re part of the function signature and affect how the compiler picks methods.
Virtual Destructors
Smart pointers have made working with C++ easier. There’s no reason to worry about memory management anymore, right?
Not so much. Let’s add destructors to our classes.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
class Foo { public: ~Foo() { std::cout << "Destroying a Foo" << std::endl; } virtual void doIt() { std::cout << "Doing foo"<< std::endl; } }; class Bar : public Foo { public: ~Bar() { std::cout << "Destroying a Bar" << std::endl; } virtual void doIt() { std::cout << "Doing bar" << std::endl; } }; |
Next, let’s allocate a Bar on the heap, use it, and then clean it up with delete.
1 2 3 4 5 6 |
int main() { Bar* bar = new Bar; Foo* fooPtr = bar; fooPtr->doIt(); delete fooPtr; } |
Now, give it a spin.
1 2 |
Doing bar Destroying a Foo |
Since we deleted our Bar instance via a pointer to Foo, and Foo’s constructor isn’t declared as virtual, the compiler called instead of the override. This can lead to leaked memory.
If you plan on using polymorphism, declare your destructors virtual.
So let’s make Foo’s destructor virtual and re-run the code.
1 2 3 |
Doing bar Destroying a Bar Destroying a Foo |
That’s more like it!
Here’s a good rule of thumb: if you plan on subclassing a class, make the destructor virtual. If you don’t, make it protected, so if someone tries to create a subclass later, the compiler will refuse to build the code.
Also, don’t create a subclass if you’re not sure that the base class has a virtual constructor. If in doubt, use composition instead of inheritance.
Deleting Arrays
We need an array of Bars.
1 2 3 4 5 6 7 8 9 |
Bar* bar1 = new Bar; Bar* bar2 = new Bar; Bar* bars = new Bar[2]; bars[0] = *bar1; bars[1] = *bar2; delete bars; |
If you’ve been coding with C++ for a while, you might see the error right away. We should delete arrays with delete[], not delete.
This code compiles. If you run a debug build, it may stop with an exception, depending on your platform. A release build may run normally, or it may exit with an error.
Here’s what I got with CLion running in Windows:
Destroying a Bar
Destroying a FooProcess finished with exit code -1073740940 (0xC0000374)
So, it exited with an error. This bug might not make it past unit tests or integration tests.
We hope.
How do you avoid this? Easy. Use a vector. Problem solved. C++’s primitive arrays are an accident waiting to happen since they act like raw pointers.
Class Members in Initialization Lists
Initialization lists are the preferred way to set up a new class instance’s state.
Here’s an example:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
class Foo { public: Foo(int size) : _size(size), _capacity(size + 2), _length(_capacity) {} virtual void status() { std::cout << "Size: " << _size << " Capacity: " << _capacity << " Length: " << _length << std::endl; } private: int _length; int _capacity; int _size; }; |
Let’s try this class out with this code in main.
1 2 |
Foo foo(50); foo.status(); |
Our output looks like this:
1 |
Size: 50 Capacity: 52 Length: 0 |
The compiler didn’t initialize the _length member correctly.
Class members are initialized in the order they are declared, not the order specified in your initialization list. Since it’s defined first, _length was initialized with the value in _capacity. But _capacity wasn’t initialized yet.
Don’t refer to class members in initialization lists, no matter how neat and concise it looks.
This is another mistake that your IDE and your static analysis tools should warn you about. But the code will still compile. It might even work sometimes.
Calling Virtual Functions in Constructors
Let’s finish up with a constructor pitfall.
First, simplify Foo’s constructor.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
class Foo { public: Foo() { status(); } virtual void status() { std::cout << "Hi there!" << std::endl; } }; |
Next, edit Bar so it only overrides the status() method. We don’t need a new constructor.
1 2 3 4 5 6 7 |
class Bar : public Foo { public: void status() { std::cout << "Yo!" << std::endl; } }; |
What happens when we create a Bar?
1 |
Hi there! |
When status() is called, our type is still Foo. So, its version of the virtual function is called.
Don’t call virtual functions in constructors.
Ignoring Your Tools
We have one more C++ pitfall to look at before we’re done.
Two of our pitfalls required ignoring the signs before we fell into the hole. When we deleted an array with the wrong operator and tried to initialize a member with another uninitialized member, both Visual Studio and CLion warned us. (I’m assuming Eclipse would have too.)
Pay attention to your tools. Run static analysis. Turn on your compiler’s warnings and tell it to treat them as errors. Your feet will thank you.
Watch Your Step
It’s possible to code in C++ without steel-toed shoes and a doctor on standby. Both the language and the tools have come a long way in the past decade, and it’s possible to get C++’s superior performance and write clean code that’s stable and easy to maintain at the same time.
TypeMock’s Isolator++ is one of those next-generation tools. You can use it to quickly put together effective tests for your legacy and your new code. Download a trial for Linux or Windows today. Your toes will thank you.
This post was written by Eric Goebelbecker. Eric has worked in the financial markets in New York City for 25 years, developing infrastructure for market data and financial information exchange (FIX) protocol networks. He loves to talk about what makes teams effective (or not so effective!)