Phase 2 · Classes, Ownership & Resource SafetyModule 10~52 min read

Constructors, Destructors & RAII

Establish valid objects and deterministic cleanup with constructors, destructors, member initialization, composition, and RAII.

What you'll learn

Constructors establish valid objects; destructors finish their lifetimes. RAII connects those two events to every resource—files, locks, memory, sockets, and handles—so cleanup follows normal scope rules instead of scattered manual calls.

By the end, you'll be able to:

  • Write default, parameterized, delegating, and explicit constructors
  • Initialize members in declaration order with initializer lists
  • Explain deterministic destruction and composition order
  • Design a small scope-bound resource owner with RAII

Constructors establish validity

A constructor has the class name and no return type. It transforms raw inputs into a valid object or reports failure; callers should never receive a half-initialized instance. Default member initializers provide sensible fallbacks, while a delegating constructor routes several entry points through one validation path.

timer.cpp
#include <stdexcept>

class Timer {
public:
    Timer() : Timer{60} {} // delegate to the validating constructor

    explicit Timer(int seconds) : remaining_{seconds} {
        if (seconds < 0) {
            throw std::invalid_argument{"negative duration"};
        }
    }

    int remaining() const { return remaining_; }

private:
    int remaining_{};
};

Key idea

Prefer “valid after construction” to a default object that requires callers to remember a later initialize() call.

Initializer lists and order

Members are initialized before the constructor body. An initializer list constructs them directly; assigning inside the body would first default-construct and then assign. Actual initialization follows member declaration order, not the textual order in the list.

session.cpp
#include <string>
#include <utility>

class Session {
public:
    Session(std::string user, int retry_limit)
        : user_{std::move(user)}, retry_limit_{retry_limit} {}

private:
    std::string user_;       // initialized first
    int retry_limit_{3};     // initialized second
};
  • References and const members must be initialized, not assigned later
  • Base classes are initialized before members
  • Members are initialized in their declaration order
  • The constructor body runs only after bases and members exist

Watch out

List initializers in declaration order. Depending on a member declared later creates a bug even when the initializer list visually suggests the opposite order.

Deterministic destruction

A destructor named ~Type() runs when an object's lifetime ends. Automatic objects are destroyed when scope exits, including exits caused by return or an exception. Members are destroyed in reverse declaration order, then base subobjects are destroyed.

scope.cpp
#include <iostream>
#include <string>

class Trace {
public:
    explicit Trace(std::string name) : name_{std::move(name)} {
        std::cout << "open " << name_ << '\n';
    }
    ~Trace() { std::cout << "close " << name_ << '\n'; }
private:
    std::string name_;
};

int main() {
    Trace outer{"outer"};
    { Trace inner{"inner"}; }
}

RAII: lifetime owns cleanup

Resource Acquisition Is Initialization means a successfully constructed object owns a resource and its destructor releases it. The object's lifetime becomes the one cleanup path. Standard streams, containers, smart pointers, and lock guards already follow this model.

line_count.cpp
#include <fstream>
#include <iostream>
#include <stdexcept>
#include <string>

int main() {
    std::ifstream input{"notes.txt"};
    if (!input) throw std::runtime_error{"cannot open notes.txt"};

    std::size_t count{};
    for (std::string line; std::getline(input, line); ) {
        ++count;
    }
    std::cout << count << '\n';
} // input closes here on every exit path

Tip

Before writing a custom owner, look for a standard RAII type. A custom destructor is often unnecessary when every member already manages its own lifetime.

A scope-bound handle

Platform APIs often return a raw handle with a matching release function. Wrap that pair immediately. The wrapper deletes copying, supports an empty state, and performs cleanup in the destructor. Later, move operations can transfer the handle safely.

file_handle.cpp
#include <cstdio>
#include <stdexcept>

class FileHandle {
public:
    FileHandle(const char* path, const char* mode)
        : file_{std::fopen(path, mode)} {
        if (!file_) throw std::runtime_error{"open failed"};
    }

    ~FileHandle() {
        if (file_) std::fclose(file_);
    }

    FileHandle(const FileHandle&) = delete;
    FileHandle& operator=(const FileHandle&) = delete;

    std::FILE* get() const { return file_; }

private:
    std::FILE* file_{};
};
The next module adds move support; std::ifstream is preferable for ordinary C++ file I/O.

Explicit, defaulted, and deleted operations

A one-argument constructor can become an implicit conversion. Mark it explicitunless that conversion is clearly part of the type's meaning. = default asks for normal generated behavior explicitly; = delete makes a forbidden operation fail at compile time with a clear contract.

SyntaxMeaning
explicit Id(int)Direct construction allowed; implicit conversion rejected
Widget() = defaultRequest the compiler's normal implementation
Widget(const Widget&) = deleteCopy construction is forbidden
~Widget() noexceptDestruction promises not to emit an exception

Destructors should not throw

If a destructor throws while another exception is already unwinding, the program terminates. Expose failure-prone completion as an explicit operation and keep destruction safe.

Recap & quick check

Key takeaways

  • Constructors should establish every invariant or fail without producing an object.
  • Initializer lists construct members directly, and actual order follows declarations.
  • Destruction is deterministic and reverses construction order.
  • RAII turns object lifetime into a reliable cleanup path for every exit route.
  • explicit, = default, and = delete make construction and special-operation policy visible.

Quick check

1. When does a constructor body run?

2. What controls member initialization order?

3. What is RAII's central connection?

4. Why mark a one-argument constructor explicit?

Next: Module 11 — Copying, Moving & the Rule of Zero, where object ownership becomes predictable value behavior.