Forward declaration and smart pointer in C++

Reading time ~3 minutes

I preach many thing when talking about programming. But the last years there are two things that is more common in my programming preaching - C++11 smart pointers and forward declaration in header files. But sometimes these two things don’t walk hand in hand. Why?

C++11 and smart pointers

I’ve already written a couple of articles about why we shall use them as much as possible, no need to go into that again.

Forward declaration

First of all, what is it?

bar.h
#pragma once

class Foo; // <- Forward declaration of Foo
class Bar {
   Bar();
   ...
private:
	Foo *m_foo;
}
bar.cpp
#include "bar.h"
#include "foo.h"

Bar::Bar() {
    m_foo = new Foo();
}

In the above example we do a forward declaration of the class Foo. Why?

There are a number of different reasons to do this, but the main being that we reduce the build dependencies between files. Instead of doing forward declaration we could have done a #include "foo.h" in the above code. That is just fine, but it adds a build dependency too foo.h. If we do a change in foo.h the above file will also be treated as “need for rebuild” which might lead to a big chain of rebuilds that is not needed. But in this case we only use Foo as a pointer and the only thing we need to know in the header is that we need to reserve space for a pointer, we don’t need any more information about Foo until we start m_foo in the source, and hence we do the `#include “foo.h” in the source file.

Doing this will help speeding up incremental builds when developing code which will reduce frustration from us developers and also save power (no planet B you know)!

Put the above two things together and you get into trouble

We can more or less take the same example above, but instead of class Foo and Bar we have Device and FooService. We also use std::unique_ptr (this is the same with std::shared_ptr and std::weak_ptr).

#pragma once

#include <memory>

class Device;

class FooService
{
public:
    FooService();

    void someFunction();
    int getInspiration();

private:
    std::unique_ptr<Device> m_device;
};

We forward declare the class Device instead of including device.h and are happy about that until we compile this code.

/usr/include/c++/7/bits/unique_ptr.h:76: error: invalid application of ‘sizeof’ to incomplete type ‘Device’
  static_assert(sizeof(_Tp)>0,
                      ^

WTF! Thanks for that error message.

You start to think!

This should work, it’s just a pointer.

This has worked before.

So let’s break this down. You might have written code just like this and got away with it. You might give up and just do the #include "device.h" because it needs the type information.

The real problem here

The real problem here is that we let std::unique_ptr handle the deletion of m_device using its default deleter function (which is great, the hole idea with std::unique_ptr). But the std::default_delete doesn’t know how big the class Device is at this point so how can it know what to delete?

Looking at the C++ reference documentation for std:unique_ptr it says:

std::unique_ptr may be constructed for an incomplete type T, such as to facilitate the use as a handle in the Pimpl idiom. If the default deleter is used, T must be complete at the point in code where the deleter is invoked, which happens in the destructor, move assignment operator, and reset member function of std::unique_ptr. (Conversely, std::shared_ptr can’t be constructed from a raw pointer to incomplete type, but can be destroyed where T is incomplete).

The simple solution

So what does this mean? Well simple - create a destructor!

#pragma once

#include <memory>

class Device;

class FooService
{
public:
    FooService();
    virtual ~FooService();

    void someFunction();
    int getInspiration();

private:
    std::unique_ptr<Device> m_device;
};
#include "device.h"
...
FooService::~FooService() {}

By just creating an empty destructor in fooservice.cpp where we included device.h we now have “created the place” were std::default_delete runs and then it knows the size of Device. This is we you might have written code that works because you had a destructor.

This was a short and simple post. Hope you like the tips! Thanks for reading!

Scoped enum:s together with bit flags/patterns

Use scoped enums to get better type safety, but this will show you how to handle the logic operations. Continue reading

Transforming singletons into something testable

Published on September 21, 2021