Logo

Blog


When an empty destructor is required

In last month's post Why you shouldn't provide an empty destructor, I taught you the difference between user-provided and user-declared. I hope you remember the conclusion; never provide an empty destructor. Either leave everything to the compiler or default the destructor (assuming there is no actual work for the destructor).

Hiding implementation details

So far, so good. I hope you followed this rule. As you probably know from life, not only programming. There is no rule without an exception. Let's talk about the one case that requires an empty destructor (yes, I know it sounds silly). Suppose you have the following code:

1
2
3
4
5
6
7
A Header file
class Apple {
  std::unique_ptr<class Orange> mOrange{};
};

B Some use after including the header
Apple a{};

In A, you have a header file. The class Orange should not be exposed at this point. Apple only uses a pointer of type Orange. A classic PImpl idiom or opaque pointer.

Later then, you use Apple in B. Without the std::unique_ptr, your code would compile. But with the std::unique_ptr, you get the following error message(s):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
In file included from <source>:2:
In file included from /opt/compiler-explorer/gcc-12.2.0/lib/gcc/x86_64-linux-gnu/12.2.0/../../../../include/c++/12.2.0/memory:76:
/opt/compiler-explorer/gcc-12.2.0/lib/gcc/x86_64-linux-gnu/12.2.0/../../../../include/c++/12.2.0/bits/unique_ptr.h:93:16: error: invalid application of 'sizeof' to an incomplete type 'Orange'
        static_assert(sizeof(_Tp)>0,
                      ^~~~~~~~~~~
/opt/compiler-explorer/gcc-12.2.0/lib/gcc/x86_64-linux-gnu/12.2.0/../../../../include/c++/12.2.0/bits/unique_ptr.h:396:4: note: in instantiation of member function 'std::default_delete<Orange>::operator()' requested here
          get_deleter()(std::move(__ptr));
          ^
<source>:4:7: note: in instantiation of member function 'std::unique_ptr<Orange>::~unique_ptr' requested here
class Apple
      ^
<source>:6:27: note: forward declaration of 'Orange'
    std::unique_ptr<class Orange> mOrange{};
                          ^
1 error generated.
Compiler returned: 1

Essentially, this message tells you that the std::unique_ptr cannot instantiate the required destructor for itself typed with Orange. The simple reason is that, at this point, Orange has no visible destructor. Which is the entire point of the PImpl idiom. What now?

Deviate from the rule (with a good reason)

It's time to violate our rule from last month. Time to provide an empty destructor for Apple.

To delay the instantiation of the unique_ptrs destructor, you must declare a destructor in Apple. That way, the destructor of the unique_ptr for mOrange gets delayed.

1
2
3
4
5
6
7
A Header file
class Apple {
  std::unique_ptr<class Orange> mOrange{};

public:
  ~Apple();
};

Next, in the implementation file for Apple, you provide the definition for the destructor.

1
Apple::~Apple() {}

I personally would prefer =default like this:

1
Apple::~Apple() =default;

But this time, the variant you use makes no difference for code generation because you look at an out-of-line definition. I would use =default for consistency reasons to avoid the empty curly braces.

Andreas