Constructors, destructors and RAII

C++ · Practical · 15 min

RAII is the central idiom of C++: it is how the language manages memory, files and locks without a garbage collector or finally blocks.

A constructor runs when an object is created and a destructor (~T()) runs when it is destroyed. For a local variable that happens deterministically: at the closing brace of its scope, in reverse order of construction, including when the scope is left by return, break or an exception.

Initialize members in the member initializer list, Trace(std::string n) : name_{std::move(n)} {}, not by assigning in the body. Members are always initialized in the order they are declared in the class, whatever order you write the list in, so keep the two consistent (-Wall warns otherwise). const members and reference members can only be set there.

Other constructor tools:

  • T() = default; keeps the compiler-generated default constructor; one constructor can delegate to another: T() : T(0) {}.
  • explicit on single-argument constructors prevents surprising implicit conversions.

RAII ("Resource Acquisition Is Initialization") means: acquire a resource in a constructor, release it in the destructor. The resource can be heap memory, a file handle, a socket, a mutex lock or a database transaction. Because destructors always run when the owning object goes away, the resource can't leak, even on error paths. You get cleanup with no finally and no manual close() calls.

The standard library is built this way: std::vector and std::string free their memory, std::ofstream closes its file, std::lock_guard unlocks its mutex, and std::unique_ptr deletes its object.

The rule of thumb: never manage a resource manually in application code. Wrap it in a class (or use an existing one), and let scope do the cleanup. Destructors must not throw; they are implicitly noexcept.

Example

#include <fstream>
#include <iostream>
#include <string>
#include <utility>

class Trace {
public:
    explicit Trace(std::string name) : name_{std::move(name)} {
        std::cout << "enter " << name_ << '\n';
    }
    ~Trace() { std::cout << "leave " << name_ << '\n'; }   // runs automatically
private:
    std::string name_;
};

void work() {
    Trace outer{"outer"};
    {
        Trace inner{"inner"};
    }                                   // leave inner
    std::ofstream log{"log.txt"};       // RAII: file opened here...
    log << "done\n";
    Trace last{"last"};
}   // leave last, then log is closed, then leave outer (reverse order)

int main() { work(); }

Remember

  • Destructors run at scope exit in reverse order, on every path including exceptions
  • RAII: acquire in the constructor, release in the destructor
  • Members are initialised in declaration order; use the member initializer list

Watch out for

  • Writing a member initializer list in a different order than the declarations and reading a member before it is initialised.

Check yourself

  1. A function creates `A a; B b; C c;` and then throws. In what order are they destroyed?
  2. Class X declares `int a; int b;` and its constructor is `X() : b{1}, a{b + 1} {}`. What is wrong?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: en.cppreference.com/w/cpp/language/raii