C++ Core Guidelines: Destructor Rules

Does my class need a destructor? I often heard this question. Most of the times the answer is no and you are fine with the rule of zero. Sometimes the answer is yes and we are back to the rule of five. To be more precise. The guidelines provide eight rules for destructors.

 1024px Max und Moritz Busch 011

Here are the eight rules:

 

Let's look at each of them in detail.

Destructor rules:

C.30: Define a destructor if a class needs an explicit action at object destruction

It's characteristic for C++ that a destructor of an object is automatically be invoked at the end of its lifetime. To be more precise, the destructor of the object is invoked when the object goes out of scope. Because of this totally deterministic behaviour, you can release highly critical resources in the destructor.

Locks or smart pointers in C++ uses this characteristic. Both will automatically release its underlying resource if they go out of scope.

void func(){
  std::unique_ptr<int> uniqPtr = std::make_unique<int>(2011);
  std::lock_guard<std::mutex> lock(mutex);
  . . .
} // automatically released

 

unipPtr releases its int and lock its mutex. Both following the RAII-idiom (Resource Acquisition Is Initialization). If you are curious about RAII, here is my post Garbage Collection - No Thanks including a remark of Bjarne Stroustrup about RAII.

You can also read the rule the other way around. If all the members of your class have a default destructor, you should not define your own.

class Foo {   // bad; use the default destructor
public:
    // ...
    ~Foo() { s = ""; i = 0; vi.clear(); }  // clean up
private:
    string s;
    int i;
    vector<int> vi;
};

 

C.31: All resources acquired by a class must be released by the class’s destructor

This rule sounds quite obvious and helps you to prevent resource leaks. Right? But you have to consider which of your class members have a full set of default operations. Now we are once more back to the rule of zero or five.

Maybe the class File has in contrast to std::ifstream no destructor and, therefore, we may get a memory leak if instances of MyClass goes out of scope.

 

class MyClass{
    std::ifstream fstream;   // may own a file
    File* file_;             // may own a file
    ... 
};

 

Zbigniew Dubil made a remark that the rule should be more specific: All resources owned by a class must be released by the class's destructor. He is right because a class can have a factory creating objects for its clients. There is no need for the destructor of the class to release the objects.

 

C.32: If a class has a raw pointer (T*) or reference (T&), consider whether it might be owning

There is a question you have to answer if your class has raw pointers or references: who is the owner? If your class is the owner, you have to delete the resource.

C.33: If a class has an owning pointer member, define a destructor

C.34: If a class has an owning reference member, define or a destructor

Rule C.33 and C.34 are quite easy to rephrase. If you own a pointer or a reference use just a smart pointer such as std::unique_ptr. std::unique_ptr is by design as efficient as a raw pointer. So you have no overhead in time or memory but only added value. Here are my posts to the details of smart pointers in C++. 

C.35: A base class destructor should be either public and virtual, or protected and nonvirtual

This rule sounds very interesting for classes having virtual functions. Let's divide it into its two parts.

Public and virtual destructor

If a class has a public and virtual destructor, you can destroy instances of a derived class through a base class pointer. The same holds for references.

struct Base {  // no virtual destructor
    virtual void f(){};
};

struct Derived : Base {
    string s {"a resource needing cleanup"};
    ~D() { /* ... do some cleanup ... */ }
};

...

Base* b = new Derived();
delete b;

 

The compiler generates for Base a non-virtual destructor, but deleting an instance of Derived through a Base pointer is undefined behaviour if the destructor of Base is non-virtual.

Protected and nonvirtual destructor

This is quite easy to get. If the destructor of the base class is protected, you can not destroy derived objects using a base class pointer; therefore, the destructor must not be virtual.

Only to make the point clear about types (not pointers or references):

  • If the destructor of a class Base is private, you can not use the type.
  • If the destructor of a class Base is protected, you can only derive Derived from Base and use Derived.
struct Base{
    protected:
    ~Base() = default;
};

struct Derived: Base{};

int main(){
    Base b;   // Error: Base::~Base is protected within this context
    Derived d;
}

 

The call Base b will cause an error.

C.36: A destructor may not fail

C.37: Make destructors noexcept

The rule which applies to C.36 and C.37 is quite general. A destructor should not fail and you should declare it, therefore, as noexcept. I think, I should say a few words about noexcept.

  • noexcept: If you declare a function such as a destructor as noexcept a exception thrown in this function will call std::terminate. std::terminate calls the currently installed std::terminate_handler, which is by default std::abort and your program aborts. By declaring a function void func() noexcept; as noexcept you state:
    • My function will not throw an exception.
    • If my function throws an exception I will not care and let the program abort.

The reason that you should explicitly declare your destructor as noexcept is quite obvious. There is no general way to write error-free code if the destructor could fail. If all of the members of a class have a noexcept destructor, the user-defined or compiler-generated destructor is even implicitly noexcept.

What's next

Maybe it sounds a little bit strange but after the rules for the destructor, the one for the constructor follows. The C++ core guidelines have about 10 rules and will write about it in the next post.

Further Information

 

 

Thanks a lot to my Patreon Supporter: Eric Pederson.

 

Get your e-book at leanpub:

The C++ Standard Library

 

Concurrency With Modern C++

 

Get Both as one Bundle

cover   ConcurrencyCoverFrame   bundle
With C++11 and C++14 we got a lot of new C++ libraries. In addition, the existing ones are greatly improved. The key idea of my book is to give you the necessary information to the current C++ libraries in about 200 pages.  

C++11 is the first C++ standard that deals with concurrency. The story goes on with C++17 and will continue with C++20.

I'll give you a detailed insight in the current and the upcoming concurrency in C++. This insight includes the theory and a lot of practice with more the 100 source files.

 

Get my books "The C++ Standard Library" and "Concurrency with Modern C++" in a bundle.

In sum, you get more than 500 pages full of modern C++ and more than 100 source files presenting concurrency in practice.

 

Add comment


My Newest E-Books

Latest comments

Subscribe to the newsletter (+ pdf bundle)

Blog archive

Source Code

Visitors

Today 54

All 429354

Currently are 140 guests and no members online