1024px Max und Moritz Busch 011

C++ Core Guidelines: Destructor Rules

Does my class need a destructor? I often heard this question. Most of the time the answer is no and you are okay 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 of C++ that a destructor of an object is automatically invoked at the end of its lifetime. More precisely, the object’s destructor is invoked when the object goes out of scope. Because of this deterministic behavior, you can release highly critical resources in the destructor.

Locks or smart pointers in C++ use this characteristic. Both will automatically release their 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

 

 

Rainer D 6 P2 500x500Modernes C++ Mentoring

  • "Fundamentals for C++ Professionals" (open)
  • "Design Patterns and Architectural Patterns with C++" (open)
  • "C++20: Get the Details" (open)
  • "Concurrency with Modern C++" (open)
  • "Generic Programming (Templates) with C++": October 2024
  • "Embedded Programming with Modern C++": October 2024
  • "Clean Code: Best Practices for Modern C++": March 2025
  • Do you want to stay informed: Subscribe.

     

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

    You can also read the rule the other way around. If all your class members 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 apparent and helps you to prevent resource leaks. Right? But you must consider which class members have a complete 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 go out of scope.

     

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

     

    Zbigniew Dubil remarked 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 class’s destructor 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

    Rules C.33 and C.34 are pretty easy to rephrase. If you own a pointer or a reference, use 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 on 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 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 behavior if the destructor of Base is non-virtual.

    Protected and nonvirtual destructor

    This is relatively easy to get. If the base class’s destructor 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 an 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 will let the program abort.

    The reason that you should explicitly declare your destructor as noexcept is quite apparent. 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 bit strange, but after the rules for the destructor, the one for the constructor follows. The C++ core guidelines have about ten rules, and I will write about them in the next post.

    Further Information

     

     

     

    Thanks a lot to my Patreon Supporters: Matt Braun, Roman Postanciuc, Tobias Zindl, G Prvulovic, Reinhold Dröge, Abernitzke, Frank Grimm, Sakib, Broeserl, António Pina, Sergey Agafyin, Андрей Бурмистров, Jake, GS, Lawton Shoemake, Jozo Leko, John Breland, Venkat Nandam, Jose Francisco, Douglas Tinkham, Kuchlong Kuchlong, Robert Blanch, Truels Wissneth, Mario Luoni, Friedrich Huber, lennonli, Pramod Tikare Muralidhara, Peter Ware, Daniel Hufschläger, Alessandro Pezzato, Bob Perry, Satish Vangipuram, Andi Ireland, Richard Ohnemus, Michael Dunsky, Leo Goodstadt, John Wiederhirn, Yacob Cohen-Arazi, Florian Tischler, Robin Furness, Michael Young, Holger Detering, Bernd Mühlhaus, Stephen Kelley, Kyle Dean, Tusar Palauri, Juan Dent, George Liao, Daniel Ceperley, Jon T Hess, Stephen Totten, Wolfgang Fütterer, Matthias Grün, Phillip Diekmann, Ben Atakora, Ann Shatoff, Rob North, Bhavith C Achar, Marco Parri Empoli, Philipp Lenk, Charles-Jianye Chen, Keith Jeffery, Matt Godbolt, and Honey Sukesan.

    Thanks, in particular, to Jon Hess, Lakshman, Christian Wittenhorst, Sherhy Pyton, Dendi Suhubdy, Sudhakar Belagurusamy, Richard Sargeant, Rusty Fleming, John Nebel, Mipko, Alicja Kaminska, Slavko Radman, and David Poole.

    My special thanks to Embarcadero
    My special thanks to PVS-Studio
    My special thanks to Tipi.build 
    My special thanks to Take Up Code
    My special thanks to SHAVEDYAKS

    Modernes C++ GmbH

    Modernes C++ Mentoring (English)

    Do you want to stay informed about my mentoring programs? Subscribe Here

    Rainer Grimm
    Yalovastraße 20
    72108 Rottenburg

    Mobil: +49 176 5506 5086
    Mail: schulung@ModernesCpp.de
    Mentoring: www.ModernesCpp.org

    Modernes C++ Mentoring,

     

     

    0 replies

    Leave a Reply

    Want to join the discussion?
    Feel free to contribute!

    Leave a Reply

    Your email address will not be published. Required fields are marked *