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.

Bjarne Stroustrup C++ meme

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.

7 thoughts on “C++ pitfalls

  1. Thank you! Excellent points. I have struggling with similar things while debugging, I wish I had read this then.

  2. A couple of these are nasty or subtle, but some of them are just PEBKAC moments.

    #1- that’s just the basic usage of references. This isn’t tricky or subtle, it’s their exact intended use case. If you were expecting anything else, then the means by which you gained those expectations is less valuable than an American subprime mortgage.

    #2- This is unfortunate. We get asked regularly about this. But it’s not really a terrible pitfall in that the compiler gives you a clear error when you try to actually use the object, and anyone who has experience in C++ knows what’s going on here. It’s an unpleasant surprise for the newcomers, but not a serious problem.

    #3- I admit this can be somewhat nasty, but most compilers will warn if your initialization list order is different to the actual initialization order.

    #4- Inlining is an implementation detail. The inline keyword actually refers to inline function definitions in source code w.r.t. ODR, not inline in any generated code (if, indeed, any code is even generated).

    #5- You asked for a Base. You got a Base. What could the result of casting to Base be, if not a Base? If you wanted a reference to the source, you should have asked for a reference. You got, entirely and exactly and quite literally, what you asked for- a Base value.

    #6- This is actually an exceedingly complicated topic. Again, this is really a compiler implementation detail that has little significance to the end-user, except that if empty classes had zero size, a lot of code dealing with memory would be substantially more complex. The Standard saves you from handling them as an annoying special case in lots of places by mandating that a class must have at least size 1. Reducing any size is, of course, a compiler-dependent optimization that is an implementation detail.

    Frankly, if you’re going to name C++ pitfalls, please pick things which are actually serious issues. It’s not like there aren’t plenty of those.

    • It may sound weird to you, but what’s stupid/simple/easy/boring to you may not be the same to someone else and sorry to break your heart, but I didn’t write this post just for you.

      If you don’t want to call them pitfalls you can call them gotchas.

      • Thanks for the notes. I keep a file of careless errors – sometimes you can sidetrack yourself looking for more complex design errors – and its always good to remember the ones that are easy to over look.

    • A bit snitty don’t you think ? This blog is not just for gurus.