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 aconstobject. - Members are initialised in declaration order, not in the order written in the initialiser list.
Special member functions
| Member | Generated when | Rule of thumb |
|---|---|---|
| Default constructor | No other constructor is declared | Write = default when you want it explicitly |
| Destructor | Always, unless you declare one | Declare one only to release a resource you own |
| Copy constructor and copy assignment | No move operation is declared | Copy the resource deeply, or delete them |
| Move constructor and move assignment | No copy operation or destructor is declared | Take the resource and leave the source valid |
operator= | Same conditions as copy | Handle 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
noexceptso 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.
Related
Modern C++ basics The standard library and smart pointers
Last refreshed 2026-09-18.