Classes, constructors and RAII

Define types that hold an invariant, understand the special member functions, and let destructors own resources deterministically.

A class with an invariant

#include <stdexcept>
#include <string>
#include <utility>

class Account {
public:
    Account(std::string owner, long opening)
        : owner_(std::move(owner)), balance_(opening) {
        if (opening < 0) throw std::invalid_argument("opening balance must be >= 0");
    }

    void deposit(long amount) {
        if (amount <= 0) throw std::invalid_argument("amount must be positive");
        balance_ += amount;
    }

    [[nodiscard]] long balance() const { return balance_; }   // const: does not mutate
    const std::string &owner() const { return owner_; }

private:
    std::string owner_;
    long balance_;
};
  • The constructor establishes the invariant: if it throws, no object is created and nothing needs cleaning up.
  • The member initialiser list (: owner_(std::move(owner))) constructs members directly; assigning in the body would be a second step.
  • [[nodiscard]] warns the caller who ignores a return value that carries meaning.
  • Mark every member function that does not modify the object as const, so it can be called on a const object.
  • Members are initialised in declaration order, not in the order written in the initialiser list.

Special member functions

MemberGenerated whenRule of thumb
Default constructorNo other constructor is declaredWrite = default when you want it explicitly
DestructorAlways, unless you declare oneDeclare one only to release a resource you own
Copy constructor and copy assignmentNo move operation is declaredCopy the resource deeply, or delete them
Move constructor and move assignmentNo copy operation or destructor is declaredTake the resource and leave the source valid
operator=Same conditions as copyHandle self-assignment and release the old resource
class Buffer {
public:
    explicit Buffer(std::size_t n) : data_(new int[n]), size_(n) {}
    ~Buffer() { delete[] data_; }

    Buffer(const Buffer &) = delete;              // ownership is unique, so no copying
    Buffer &operator=(const Buffer &) = delete;

    Buffer(Buffer &&other) noexcept              // a move takes the resource over
        : data_(other.data_), size_(other.size_) {
        other.data_ = nullptr;
        other.size_ = 0;
    }

    Buffer &operator=(Buffer &&other) noexcept {
        if (this != &other) {                    // guard self-move
            delete[] data_;
            data_ = other.data_;
            size_ = other.size_;
            other.data_ = nullptr;
            other.size_ = 0;
        }
        return *this;
    }

private:
    int *data_;
    std::size_t size_;
};
💡
This class follows the rule of five: if you declare any of the destructor, copy or move operations, declare all of them consistently. In new code prefer the rule of zero — store a std::unique_ptr or a std::vector and declare none of them.

RAII in practice

RAII means a resource is acquired in a constructor and released in a destructor. Destructors run when a scope exits, including while an exception unwinds the stack, so cleanup stops being something you can forget.

void process() {
    std::ifstream in("data.txt");        // opens here
    if (!in) throw std::runtime_error("cannot open data.txt");

    std::vector<Record> records = load(in);

    std::lock_guard<std::mutex> guard(mutex_);   // unlocks on every exit path
    cache_.update(records);
}                                        // in closes, guard unlocks, records frees

// the same idea applied to something custom
class Stopwatch {
public:
    explicit Stopwatch(const char *label) : label_(label), start_(Clock::now()) {}
    ~Stopwatch() { log(label_, Clock::now() - start_); }
private:
    const char *label_;
    Clock::time_point start_;
};
  • The destructor runs in reverse order of construction, so later objects are released first.
  • A destructor should not throw: if it does while another exception is in flight, the program terminates.
  • Mark move constructors noexcept so containers can move rather than copy when they grow.
  • Any pair of acquire and release calls — file, lock, socket, transaction — belongs in a type that owns it.

FAQ

Why not just allocate the object with <code>new</code>?
A heap object is not released by scope exit, so you must delete it on every path, including the ones an exception takes. Stack objects and smart pointers do that for you.
When does a class need a virtual destructor?
Whenever the class is designed to be deleted through a base-class pointer. Without one, deleting a derived object through a base pointer runs only the base destructor and leaks whatever the derived class owned.

Modern C++ basics The standard library and smart pointers

Last refreshed 2026-09-18.