9 most common pitfalls every C++ programmer eventually falls into
C++ is an old and complex language with a lot of legacy and dark avenues where problems lurk. Aside of very obscure and arcane problems you might experience when working with C++, there’s a set of very common ones which eventually everyone will come across. So, let’s have some fun and name a few.
Most vexing parse
Most vexing parse is a declaration ambiguity in the language. What might look like a variable declaration, really is a function declaration. Here’s an example:
|
|
numbers
is actually a function prototype. This won’t compile but thankfully, new compilers will warn you about the problem:
|
|
This problem has been remedied with list
initialisation
introduced in C++11 and just as the compiler suggests, parentheses have to be
replaced with {}
to fix the problem and remove the ambiguity.
{}
instead of ()
to enforce object initialisation and eliminate most vexing parse ambiguity.Why not just declare as: std::vector<int> numbers;
? Well that brings me to my second point.
Expecting zero initialisation to always happen for free
Zero initialisation is not done automatically for all declarations. There’s a significant difference between the below:
|
|
C++’s main principle which is
you only pay for what you use
is very much applicable here.
This is not a huge problem with std::vector
as it has a set of constructors
making sure that it’s always instantiated with a well defined state but in case
of aggregates with no constructors, there’s a difference. Consider the following:
|
|
{}
to enforce zero-initialisationconst shared_ptr&
is not const
Before talking about shared_ptr
let’s do a short pointers grammar recap.
Pointer to constant data
const int* p
- p
is a pointer to constant int
. The pointer itself
can be changed, the data it points to can’t be altered.
|
|
Constant pointer to data
int* const p
- p
is a constant pointer to int
. The pointer itself
can’t be changed, the data it points to can be changed without any
problems.
|
|
Constant pointer to constant data
const int* const p
- p
is a constant pointer to constant int
. Both the
data and the pointer are constant. Nothing can be changed.
|
|
With the above out of the way, let’s go back to std::shared_ptr
. One would
expect similar semantics when using a const
reference to a smart pointer i.e.:
|
|
The assignment works because operator*
is actually
const
.
Therefore operator*
guarantees that shared_ptr
won’t be mutated but it
returns T*
so, data can be altered with no limits.
As expected, the pointer can’t be re-seated as swap
is not a const
function.
const shared_ptr&
still allows to modify the pointee.Calling shared_from_this
from constructors
On a note of shared_ptr
, I’ve been bitten in the past, trying to use
shared_from_this
in a constructor. Consider the following:
|
|
This will crash. The reason is simple. std::enable_shared_from_this
internally contains a std::weak_ptr
to Foo
. It cannot obtain this
pointer though until the construction is finished. So, an attempt to use
shared_from_this
in constructor is a bit of a catch 22 situation - you want
to use shared_from_this
- which locks a shared_ptr
from weak_ptr
but that
weak_ptr
is only going to be set once you’re done with construction.
shared_from_this
in a constructor just returns a nullptr
initialised shared_ptr
.The usual workaround is to do two stage initialisation like so:
|
|
shared_from_this
is required during construction.Using virtual functions in constructors/destructors
Constructors and destructors are truly a special functions that should always be treated with care and attention. One might be surprised that the following:
|
|
Will actually produce:
|
|
Instead of expected Derived::foo
. Why does that happen?
The construction of Derived
will begin once the Base::constructor
is done
so, it’s not possible to call anything from Derived
at that stage as it’s in undefined state, therefore as a rule of thumb
What happens if we attempt to call a pure virtual function?
|
|
This is an undefined behaviour very well described in the C++ standard. The compiler will issue a warning (this should be a compile error, in my opinion, but unfortunately it’s not) in such instance:
|
|
Although this is an undefined behaviour, on most platforms you can expect either a SIGABRT similar to this one:
|
|
or just a link-error (which is probably a much better outcome):
|
|
Confusion around universal/forwarding and rvalue references
Universal references are a completely different beast than rvalue references. Depending on the value provided, they can be easily converted to rvalue or lvalue references to implement perfect forwarding. It’s easy to mislead one for another though.
|
|
Here’s some more examples:
|
|
Throwing exceptions from destructors
Implicitly all destructors are marked as noexcept
unless there are class data
members or base classes that have a noexcept(false)
destructor but, in general,
throwing an exception from a destructor is allowed by the language and is a
valid, defined behaviour. However, it is problematic and should be avoided at all cost!
The destructors are called due to object’s lifetime end. This may happen due to an explicit deletion, scope exit, ownership release or any other reason. Scope exit itself may occur naturally or as a result of an already thrown exception.
Consequently, it’s quite likely that throwing in a destructor will result in
throwing during an already happening exception handling and this leads to
std::terminate
. This is very well described in both
throw keyword documentation and
a page detailing exceptions and exception safety.
Most C++ programmers know about this gotcha and, in general, avoid using
throw
directly in the destructor. It’s easy to forget though about all other
functions that you’re calling in the destructor that may not necessarily be
noexcept(true)
and may throw so, as a good measure, it’s good to wrap the
destructor’s body with a try/catch
block like so:
|
|
Since the body of the destructor is formed entirely of a try/catch
block, you
might be tempted to just use the
function-try-block
but there’s another gotcha. The snippet below behaves differently than the prior one:
|
|
First of all:
Before any catch clauses of a function-try-block on a destructor are entered, all bases and non-variant members have already been destroyed.
So, to re-phrase, all non static class data members will be destroyed prior to entering any of the catch
blocks.
Conversely, with try/catch
in the destructor’s body, these will still be available.
Additionally (and this one is very important):
Reaching the end of a catch clause for a function-try-block on a destructor also automatically rethrows the current exception as if by
throw;
, but a return statement is allowed.
As suggested, you need a return
to make it work as expected:
|
|
Throwing exceptions from deleter functions
You might be tempted to just throw an exception when overriding a delete
operator or providing a custom deleter for a smart pointer but this should
never be done. Custom deleter functions for e.g. unique_ptr
require that the deleter is a nothrow CopyConstructible callable:
unique_ptr(pointer p, const A& d) noexcept;
(1) (requires that Deleter is nothrow-CopyConstructible)
unique_ptr(pointer p, A&& d) noexcept;
(2) (requires that Deleter is nothrow-MoveConstructible)
As for delete
operator - most
of these have noexcept(true)
in their specification. How to handle de-allocation errors then?
Consider using std::exit
- which will allow destruction of thread local
storage objects, global static variables and will result in execution of
functions registered with std::atexit
.
|
|
std::exit
to indicate fatal error.Forgetting to use virtual destructors
When using virtual inheritance it’s very important to use virtual
destructors
in base classes otherwise, you may have to deal with resource leaks. Consider this example:
|
|
In the example above, the destruction of Derived
is happening through a
Base
class pointer. Since the Base
class’s destructor is not virtual, only
Base::~Base
will be called. Derived::~Derived
won’t be executed - leading
to resource leaks or clean-up problems.
To solve that problem ~Base
has to be marked as virtual:
|
|
virtual
to rely on polymorphic destruction.