Memory and RAII
C++‘s memory model is C’s plus RAII — Resource Acquisition Is Initialisation: every resource is represented by an object whose constructor acquires the resource and whose destructor releases it. Programs do not call delete directly in modern C++; the lifetime is managed by the type system through the construction-and-destruction discipline of class types. The principle generalises beyond memory to any resource that requires release: file handles, sockets, mutexes, database connections, OpenGL contexts. The technique is the single most important idiom that distinguishes C++ from C; nearly every other distinctive C++ feature — exceptions, smart pointers, the standard containers — is built on it.
This page covers the storage durations, the new/delete operators, the rule-of-N constructs, the smart-pointer machinery, and the common defects that RAII is designed to prevent.
Storage durations
The standard recognises four storage durations, the same set as C with thread storage given more prominence:
- Automatic — the default at block scope. Storage is reserved on entry, released on exit. The conventional implementation is the stack.
- Static — namespace-scope objects, and block-scope objects declared
static. Storage exists from program start to program termination. Initialisation occurs once. - Thread — objects declared
thread_local. One instance per thread, with start-to-end-of-thread lifetime. - Dynamic (the standard’s term for allocated) — memory obtained from the allocator (
new,make_unique,make_shared, container internals). Lifetime is bounded explicitly.
int global = 0; // static duration
void f() {
int local = 0; // automatic
static int total = 0; // static, but block-scope visibility
thread_local int per_thread = 0; // thread duration
int *heap = new int(0); // dynamic; must be released
delete heap; // explicit release
}
Modern C++ minimises raw new/delete by binding dynamic-duration objects to automatic-duration owner objects (smart pointers, container types). The owner’s automatic destruction releases the dynamic-duration object.
new and delete
The bare allocator-and-constructor pair:
Widget *w = new Widget(arg1, arg2);
// ...
delete w;
int *arr = new int[100]();
// ...
delete[] arr;
new performs two operations: it allocates raw memory (via operator new, akin to malloc) and constructs an object in that memory. delete performs the corresponding two: destruct, then deallocate.
The single-object and array forms must be matched: new T pairs with delete; new T[n] pairs with delete[]. Mismatches are undefined.
Placement new
new admits a placement form that constructs at a caller-provided address:
char buffer[sizeof(Widget)];
Widget *w = new (buffer) Widget(arg1, arg2);
// ...
w->~Widget(); // explicit destruction; no deallocation
The construction is principally useful for custom allocators, object pools, and cases where the allocation and the construction must be separated (for example, lazy initialisation of a member). The destructor must be invoked explicitly; there is no delete-form because no allocation occurred.
Failure
new throws std::bad_alloc on failure by default. The non-throwing form returns nullptr:
Widget *w = new (std::nothrow) Widget(args);
if (!w) {
// allocation failed
}
The throwing form is conventional in C++ code; the non-throwing form is reserved for code that must not throw (kernel-level, embedded, certain exception-free codebases).
RAII as the organising principle
The Resource Acquisition Is Initialisation idiom binds a resource to an object’s lifetime. The constructor acquires the resource; the destructor releases it. Because C++ guarantees destructor execution at scope exit (whether by normal control flow, exception propagation, or return), the resource is released deterministically.
class FileHandle {
public:
explicit FileHandle(const std::string &path)
: fp_(std::fopen(path.c_str(), "r"))
{
if (!fp_) throw std::runtime_error("cannot open " + path);
}
~FileHandle() { if (fp_) std::fclose(fp_); }
FileHandle(const FileHandle&) = delete;
FileHandle &operator=(const FileHandle&) = delete;
FileHandle(FileHandle &&other) noexcept : fp_(other.fp_) { other.fp_ = nullptr; }
FileHandle &operator=(FileHandle &&other) noexcept {
if (this != &other) {
if (fp_) std::fclose(fp_);
fp_ = other.fp_;
other.fp_ = nullptr;
}
return *this;
}
std::FILE *get() const { return fp_; }
private:
std::FILE *fp_;
};
void process(const std::string &path) {
FileHandle f(path); // acquires the file
// … use f.get() …
} // f's destructor closes the file
The class encapsulates the resource, prevents copying (which would close the file twice), permits moving (transferring ownership), and guarantees release at scope exit, regardless of how the scope is left.
The pattern generalises: every resource type — std::unique_ptr<T> for memory, std::lock_guard<Mutex> for locks, std::ofstream for output files, std::thread for threads — is built around it. Most user-defined resource types are written by adapting the same template.
The rule of zero, three, and five
A class that owns a resource must consider five special member functions:
| Function | Signature |
|---|---|
| Destructor | ~T() |
| Copy constructor | T(const T&) |
| Copy assignment | T& operator=(const T&) |
| Move constructor | T(T&&) noexcept |
| Move assignment | T& operator=(T&&) noexcept |
The compiler generates each by default if not declared. The defaults are correct only for trivially copyable types; types that own a resource (a pointer to a heap allocation, a file descriptor, a mutex) require explicit definitions.
The rule of zero
If a class does not directly manage a resource — instead delegating to members that do (for example, a std::unique_ptr member) — it should declare none of the five. The defaults are correct, and the class composes safely with whatever its members provide.
class Document {
public:
Document(const std::string &content) : content_(content) {}
// no other special members declared; defaults are correct
private:
std::string content_;
std::unique_ptr<Compiled> compiled_;
std::vector<Reference> references_;
};
The class is value-semantic, copyable, movable, and destructible — all by default — because each member is itself well-behaved.
The rule of three (C++03)
If a class declares any of the destructor, copy constructor, or copy assignment, it should declare all three. The reason: any class that manages a resource almost certainly needs to define all three to handle the resource correctly.
The rule of five (C++11)
C++11 added move construction and move assignment. The rule of three becomes the rule of five: if a class defines any of the five, it should consider defining all five. The defaults for move are particularly subtle — a class that defines a copy constructor inhibits the implicit move constructor — and the explicit declaration is the conventional defence.
In practice, most classes follow the rule of zero: the resource-owning members handle their own special functions, and the enclosing class needs none. The rule of five is needed only for classes that directly own a resource.
Smart pointers
The standard-library smart pointers in <memory> are the conventional way to express ownership of dynamically-allocated objects. They are RAII wrappers around raw pointers.
std::unique_ptr<T>
Sole ownership; non-copyable; movable; deletes the object on destruction.
auto w = std::make_unique<Widget>(arg1, arg2);
void take_ownership(std::unique_ptr<Widget>);
take_ownership(std::move(w)); // w is now empty; the callee owns
std::make_unique<T>(args...) is the canonical construction primitive: it allocates and constructs in one expression, with no opportunity for a leak between the two steps.
The conventions:
- A function that returns owned-heap-objects returns
std::unique_ptr<T>. - A function that takes ownership accepts
std::unique_ptr<T>by value. - A function that observes but does not take ownership accepts
T*orT&(orconst T&). - A function that may take ownership conditionally accepts
std::unique_ptr<T>and decides at the call site (rare).
std::shared_ptr<T>
Shared ownership through reference counting; the object is deleted when the last shared_ptr is destroyed.
auto p1 = std::make_shared<Widget>();
auto p2 = p1; // copies; reference count increments
p2.reset(); // p2 abandons; ref count decrements
// p1 still owns
The reference count is atomic, so copying a shared_ptr across threads is safe. The cost is the atomic increment-and-decrement on each copy and the indirection through the control block; shared_ptr should not be the default. The cases where it is appropriate:
- Genuinely shared ownership across an indeterminate set of owners.
- Nodes in a graph that may have multiple parents.
- Objects whose lifetime is determined by the union of multiple owners.
For sole ownership, unique_ptr is the right choice. For non-owning references, raw pointers or references are the right choice.
std::weak_ptr<T>
A non-owning observer of a shared_ptr-managed object. Used principally to break ownership cycles.
class Node {
std::vector<std::shared_ptr<Node>> children;
std::weak_ptr<Node> parent; // breaks the cycle
};
To access the pointed-to object, the weak_ptr is converted to a shared_ptr via lock(), which returns an empty shared_ptr if the object has been destroyed.
The free store
The implementation maintains a free store (the C++ name for the heap), accessed through operator new and operator delete. These are global functions that the user may replace; the conventional replacement is for instrumentation, performance profiling, or custom allocator strategies.
The standard distinguishes:
| Function | Purpose |
|---|---|
operator new(size_t) | Single-object allocation. |
operator new[](size_t) | Array allocation. |
operator delete(void*) | Single-object deallocation. |
operator delete[](void*) | Array deallocation. |
operator new(size_t, std::align_val_t) | Aligned allocation (C++17). |
User-defined operator new may be defined as a class member, in which case it is invoked when the class is allocated with new. The mechanism is the conventional way to implement object pools and custom allocators for a specific class.
Custom allocators
The standard library’s container types are parametrised by an allocator type that controls how the container obtains and releases memory. The default is std::allocator<T>, which forwards to operator new/operator delete. Custom allocators are used for:
- Embedded systems where the heap must be a fixed-size pre-allocated region.
- Performance-critical paths where the conventional allocator is too slow (per-thread allocators, region allocators).
- Memory-tagging or instrumentation use cases.
C++17 introduced <memory_resource> (the polymorphic memory resource facility), which provides a runtime-dispatched allocator interface that is easier to use across heterogeneous code than the template-based allocators. The conventional advice is to use the default allocator unless measurement or design constraints justify otherwise.
Common defects
The recurring memory-related defects in C++:
| Defect | Description |
|---|---|
| Leak | A new without a matching delete. Most commonly: an exception thrown between allocation and assignment to a unique_ptr. The defence is make_unique. |
| Use after free | Access through a pointer after delete (or after the owning unique_ptr is destroyed). Undefined behaviour. |
| Double free | Two delete calls on the same pointer. Often: copying a class that owns a raw pointer without defining the copy constructor. |
Mismatched new/delete | new T and delete[] p (or vice versa). |
| Slicing | Copying a derived class through a base-class value; the derived parts are lost. The defence is to keep base classes non-copyable or to use polymorphic types only through pointers/references. |
| Self-assignment in copy/move assignment | t = t corrupts state if the assignment is not self-assignment-safe. Defence: check this != &other for non-trivial cases. |
| Resource leak in constructor | A constructor that allocates one resource, then throws while allocating another. The first resource is leaked unless owned by a sub-object whose destructor will run. The defence is to wrap each resource in its own RAII type. |
| Dangling pointer/reference | A pointer or reference to an object whose lifetime has ended. Common with string_view and span whose source has been freed. |
The combination of RAII, smart pointers, the standard containers, and modern sanitisers (-fsanitize=address, -fsanitize=undefined, the equivalents in Clang and GCC) catches or prevents the substantial majority of these defects. The cases that remain — slicing, dangling views, ownership cycles in shared_ptr — are reduced by careful API design rather than by tooling alone.