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:
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
=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:
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.
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.
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
A good reason for providing a destructor is if the destructor needs to be
virtual. In the case of
I know one more good reason for
=default, but more about next month.