The Pitfalls of C++

Here’s a post I wrote about my other working langauge, C++. You can find the original over on Typemock.

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.

Next, we have a mainfunction that accesses the subclass via a pointer to the base.

Now, when we run the program, we see this:

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().

Then, run the program again.

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.

Next, run this new version of our test program:

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.

Next, let’s allocate a Bar on the heap, use it, and then clean it up with delete.

Now, give it a spin.

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.

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.

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 Foo

Process 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:

Let’s try this class out with this code in main.

Our output looks like this:

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.

Next, edit Bar so it only overrides the status() method. We don’t need a new constructor.

What happens when we create a Bar?

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.

Leave a Reply

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Proudly powered by WordPress | Theme Name: BlogEasy by ThemesPoint.
%d bloggers like this: