C++ pitfalls

C++ is a powerful language, but “with great power comes great responsibility” so it’s quite easy to mess things up and to make mistakes that can lead to unexpected results and miserable fails.

In this post I’m going to describe 8 not-so-obvious aspects of C++ that can lead to bugs or to code which doesn’t even compile.

This post has been inspired by a great book about C++: “Effective C++: 55 Specific Ways to Improve Your Programs and Designs (3rd Edition)” by Scott Meyers and by the C++ FAQ maintained by Marshall Cline, and obviously I strongly recommend C++ coders to read both.

Before starting I should probably point out this post is not intended for an expert audience, so if you’re a professional C++ coder you probably won’t get much from it, reading it won’t hurt you though. 😉

The tricky reference

Let’s start with something simple: references.

A reference is an alias for an object and from a low level point of view it’s pretty similar to a pointer. References and pointers are quite different though, and I’m going to show you this with a very simple example based on the following class:

class Base { public: Base(int v) : _val(v) { }; void Print() { cout << _val << endl; }; private: int _val; };

We can create two Base objects and assign one of them to a reference:

Base b1(10); Base b2(20); Base & rb = b1;

Then let’s print their value with the following code:

b1.Print(); b2.Print(); rb.Print();

As expected, the result will be:

10 20 10

But if we try to reassign our reference:

rb = b2;

Printing the values again will give us something different this time:

20 20 20

And that’s because the previous line of code was equivalent to the following one:

b1 = b2;

So object b2 was copied into object b1, whereas rb is still an alias for b1.

The lesson here is you can’t reseat references, once they are assigned to an object they stay with it.

The constructor which is not a constructor

A “default constructor” is a constructor that can be called with no arguments, for example:

B() { };

could be a default constructor of the class B.

You may be tempted to create an object of type B using the following code:

B obj();

Unfortunately that line of code is not calling the default constructor of B, it’s declaring a function which doesn’t take any parameter and returns a B object, probably not what you wanted.

The proper way of creating an object of class B calling the default constructor is:

B obj;

It’s a matter of order

Constructors should always use initialization lists, but they could give you some headache like in the following class:

class B { public: B(int v) : _c(v), _a(_c + 1) { cout << "B - _a: " << _a << " , _c: " << _c << endl; }; private: int _a; int _c; };

Now if you try to create an object of class B:

B obj(10);

You may get disappointed by the result:

B - _a: 1 , _c: 10

which probably is not what you expected (_a = 11 , _c = 10).

The problem is that data members in an initialization list are always initialized in the order they are declared in the class, so in this case: _a first, then _c.

The best way to rewrite the constructor of B is:

B(int v) : _a(v + 1), _c(v) { cout << "B - _a: " << _a << " , _c: " << _c << endl; };

So the rules here are:

always initialize data members in the same order they are declared in their class

avoid to use data members to initialize other data members

Inline functions never inline

Inline functions are functions that a compiler may expand in the calling code in a similar way the code of a #define macro is replaced in the source code using it. I highlighted the word “may” as the final decision to expand the code or not is entirely up to the compiler.

A rule of thumb is to keep inline functions short and simple, hoping the benevolent compiler will accept our request and will expand the code of the inline functions in all the places where it’s called.

According to the rule of thumb above, a function like this should be a good candidate for proper inlining:

virtual void Inc() { _val++; };

Unfortunately the code of that function will (probably) never be embedded in any calling code, instead the function will be considered by the compiler as a regular one.

That doesn’t happen because the compiler is evil, but because the function is virtual, which basically involves some decision at runtime, whereas the inlining happens at compile time making virtual functions not a good candidate for it.

Casting gone wrong

Casting in C++ is a potential source for many errors, but some of them can be quite subtle.

For example starting from the following classes:

class Base { public: Base() : _val(0) { }; virtual void Inc() { _val++; }; protected: int _val; }; class Derived : public Base { public: virtual void Inc() { _val += 2; }; };

Someone could write some code like this:

Derived d; static_cast<Base>(d).Inc();

The value of _val is 0 when d is created, but can you imagine what it will be after calling Inc()? 0, 1 or 2?

The right answer is… 0!

Apparently Inc() has never been executed, instead it has, it wasn’t called on d though, but on a temporary object created by the cast. so everything in the object d is unchanged after that line of code.

Size of an empty class

Any idea what’s the size of an empty class?

class Empty { };

The answer is 0 of course…

Nah, just joking, it’s 1 and that’s because C++ doesn’t allow any object of size 0.

Actually some compilers may add some padding and the size of an empty class could be 4 or even 8, but that’s not something I experienced during my tests.

So, considering 1 the size of an empty class, what’s the size of an empty derived class?

class Derived : public Empty { };

2 maybe? Nah, still 1.

But Empty is missing something, a virtual destructor! So if we make things right:

class Base { public: virtual ~Base() { }; }; class Derived : public Base { };

The size of Derived is now… 4 (or 8 if compiling for 64 bit) and that’s because the class needs to keep a pointer for the virtual table which is used for virtual functions.

What if we added few non-virtual functions to Derived? The size will remain exactly the same, as functions are not data members and don’t take any size,

Hiding inherited names

Virtual functions allow derived classes to change the implementation of functions declared in a base class. For example:

class Base { public: virtual void f1() { cout << "Base::f1()" << endl; }; virtual void f1(int a) { cout << "Base::f1(int) : " << a << endl; }; }; class Derived : public Base { public: virtual void f1() { cout << "Derived::f1()" << endl; }; };

In the above code Derived::f1() redefines Base::f1(), but it ignores Base::f1(int), so what do you think it will happen with the following code?

Derived d; int x = 10; d.f1(); d.f1(x);

Nothing.

That code will never be executed because the compiler will never compile it, it will raise an error saying something like: “no matching function for call to ‘Derived::f1(int&)’“.

That happens because Derived re-implementing f1() is also hiding Base::f1(int) to the compiler.

In order to fix this problem we need to change Derived as follow:

class Derived : public Base { public: using Base::f1; virtual void f1() { cout << "Derived::f1()" << endl; }; };

now the code will compile and it will print the following text:

Derived::f1() Base::f1(int) : 10

As expected.

Default parameters in derived classes

Sometimes functions with default parameters can lead to unexpected results, this is especially true when such functions are also virtual like in the following classes:

class Base { public: virtual void f(int val = 0) { cout << "Base::f - val: " << val << endl; }; }; class Derived : public Base { public: virtual void f(int val = 1) { cout << "Derived::f - val: " << val << endl; }; };

Trying to use those classes as follow:

Derived d; Base * pb = &d; pb->f(); pb->f(10);

Will print the following text:

Derived::f - val: 0 Derived::f - val: 10

Basically the default parameter value defined in Derived::f(int) has been totally ignored and replaced by the value defined in Base::f(int).

That happens because default values are bounded at compile time, whereas the proper virtual function to call is determined dynamically at runtime.

The only way to prevent such error is to not redefine default values for virtual functions.

And that’s all for now!

I’ll probably be back on the subject in the future as there’s always something to say about it, but in the meanwhile feel free to share your experiences posting a comment.