Why you shouldn't provide an empty destructor
Today's post is a written version of C++ Insights episode 38 I published back in May. I decided to write this post to be able to use what I teach you in next month's post. In case you prefer the video here, it is:
The scenario
I see code like the below way too often in code reviews for my taste.
1 2 3 4 |
|
What do I dislike? Well, the destructor which doesn't do anything, or does it? I get different answers when I point out such a construct during a review. One that frequently comes up is that people prefer seeing that this class has a destructor. When I ask why, not just use =default
, things get interesting. There is one group that simply says old habits I forgot. Another group tells me that =default doesn't make a difference to the code presented above. Let's explore what's happening.
Yes, there are scenarios where you might not notice any difference between {}
and =default
for a destructor. But technically, there is one, and you can observe this difference.
Suppose the class Apple
from above is used with a std::optional
like this:
1 |
|
Not that bad, right? Well, how about we briefly examine the resulting assembly code? (I know I created C++ Insights to avoid looking at assembly code, but this time there is no way around it :-). Here is the output from Compiler Explorer:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
As you can see, there is a destructor Apple::~Apple
present in the assembly code. Further, you can spot two calls to that destructor, which is doing absolutely nothing. At least nothing we need.
User-provided vs. user-declared
All the above boils down to the difference between user-provided and user-declared. In the first example, the destructor of Apple
counts as user-defined. While technically, we look at a declaration and definition, the definition part is the one that makes the difference. We tell the compiler that the implementation, which is none here, comes from us. As a result, the class no longer counts as trivially destructible.
For the optional
example, the library provides a perfect wrapper around your type. Since Apple
is not trivially destructible, the optional
creates code to invoke the destructor of Apple
. While there might be scenarios when the compiler and optimizer can see through that, we are entering optimization land without any guarantees.
Now, what is user-declared then? We can get user-declared by using =default
for the destructor like this
1 2 3 4 5 6 |
|
That way, the class still counts as trivially destructible, assuming all members are also trivially destructible. The resulting assembly output changes since nothing has to be done for a trivially destructible object. Here is the output again from Compiler Explorer:
1 2 3 4 5 6 7 8 9 10 |
|
Et voilà, no code or calls for a destructor anymore.
Conclusion
Another reason to be cautious with the destructors is when you want your class to be moveable. See my post A destructor, =default, and the move operations for further insights.
In case there is no good reason, prefer leaving the special members untouched. Especially the destructor. If you really must provide an empty destructor, use =default
.
A good reason for providing a destructor is if the destructor needs to be virtual
. In the case of ~Apple
:
1 |
|
I know one more good reason for =default
, but more about next month.
Andreas