Summary
C++ manages resources without a garbage collector through RAII — Resource Acquisition Is Initialization. The rule is simple: acquire a resource in a constructor; release it in a destructor. When an object goes out of scope, its destructor runs automatically, releasing all resources. For heap-allocated objects that outlive a scope, the standard library provides unique_ptr and shared_ptr to automate deletion. Move semantics allow ownership of resources to be transferred cheaply between objects.
(Stroustrup, A Tour of C++, 3rd ed., Chs. 5–6, 15, see source-a-tour-of-cpp)
The free store (heap)
Stack objects are destroyed at scope exit automatically. For objects whose lifetime must outlast their creating scope, C++ provides the free store (heap):
int* p = new int{42}; // allocate one int on the heap
delete p; // release it (required!)
int* arr = new int[10]; // allocate array
delete[] arr; // release array (must match new[])The problem: new and delete are manual. Forgetting delete causes memory leaks; deleting too early causes use-after-free bugs; deleting twice is undefined behaviour.
The solution is to avoid naked new and delete entirely. RAII and smart pointers make them unnecessary in application code.
RAII
Resource Acquisition Is Initialization (RAII) is the fundamental C++ resource management idiom. A resource — memory, a file handle, a mutex, a socket, a thread — is wrapped in an object. The constructor acquires the resource; the destructor releases it. The resource’s lifetime is tied to the object’s lifetime.
class Vector {
public:
Vector(int s) : elem{new double[s]}, sz{s} {} // acquire: allocate array
~Vector() { delete[] elem; } // release: free array
private:
double* elem;
int sz;
};
void f()
{
Vector v(1000);
// ... use v ...
} // v.~Vector() runs here; memory releasedEven if an exception is thrown inside f(), C++ guarantees that all local objects are destroyed (their destructors run) when the scope is exited. This is stack unwinding and it makes RAII exception-safe automatically.
What RAII covers (from the standard library):
- Memory:
string,vector,map,unique_ptr,shared_ptr - Files:
ifstream,ofstream - Threads:
thread - Locks:
scoped_lock,unique_lock
C# comparison: C# uses a garbage collector for memory and IDisposable/using for non-memory resources. C# garbage collection is non-deterministic — you cannot know exactly when an object is freed. C++ destructors run deterministically at scope exit.
Destructors
A destructor has the name ~ClassName(). It takes no arguments, returns nothing, and cannot be overloaded. It runs automatically when the object is destroyed:
- Stack object: at closing
}of its scope new-allocated object: whendeleteis called on it- Member of another object: when the containing object is destroyed
class FileHandle {
public:
FileHandle(const string& name) : f{fopen(name.c_str(), "r")} {}
~FileHandle() { if (f) fclose(f); } // RAII: close on destruction
private:
FILE* f;
};Virtual destructors: if a class has virtual functions, its destructor should also be virtual. Without it, deleting a derived object through a base pointer only runs the base destructor:
class Shape {
public:
virtual ~Shape() {} // required: correct destruction through base pointer
};Copy semantics
By default, copying an object performs memberwise copy — each member is copied individually. For objects that own heap resources, this is wrong: two copies would point to the same data, and both would try to delete it.
Fix with a copy constructor and copy assignment operator:
class Vector {
public:
Vector(const Vector& a); // copy constructor
Vector& operator=(const Vector& a); // copy assignment
};
Vector::Vector(const Vector& a)
: elem{new double[a.sz]}, sz{a.sz}
{
for (int i = 0; i != sz; ++i)
elem[i] = a.elem[i];
}After a proper copy, each object has its own copy of the data — modifying one does not affect the other.
Move semantics
Copying a large container is expensive. When the source is a temporary (or no longer needed), we can move instead — transferring the resource without copying:
class Vector {
public:
Vector(Vector&& a) // move constructor
: elem{a.elem}, sz{a.sz}
{
a.elem = nullptr; // leave source in a valid but empty state
a.sz = 0;
}
};Vector&& is an rvalue reference — a reference to an object that is about to be destroyed or explicitly moved. The move constructor steals the data from the source, leaving it empty.
std::move(): explicitly converts an lvalue to an rvalue reference, enabling move semantics:
Vector y = std::move(x); // x is now empty; y owns the data
// do not use x after thisWhy this matters: returning a vector<int> from a function is fast — the compiler elides the copy or uses the move constructor. Without move semantics, returning large containers by value required expensive copies.
The rule of zero / rule of five
If a class requires a custom destructor, it almost certainly also needs custom copy and move operations:
class X {
public:
X(); // default constructor
X(const X&); // copy constructor
X& operator=(const X&); // copy assignment
X(X&&); // move constructor
X& operator=(X&&); // move assignment
~X(); // destructor
};Rule of zero: if a class uses only standard-library types for its members (e.g., vector, string, unique_ptr), define none of these — the compiler generates correct versions automatically.
Rule of five: if you define any one of the five (copy constructor, copy assignment, move constructor, move assignment, destructor), you probably need to define all five explicitly.
Use = default to explicitly request compiler-generated versions, and = delete to prohibit them:
class Shape {
public:
Shape(const Shape&) = delete; // no copying
Shape& operator=(const Shape&) = delete;
virtual ~Shape() {}
};Smart pointers
The standard library provides two owning pointer types that implement RAII for heap-allocated objects:
unique_ptr<T> — sole ownership
#include <memory>
auto p = make_unique<Circle>(center, radius); // preferred: no naked new
unique_ptr<Circle> p2 {new Circle{center, r}}; // also valid
p->draw(); // access like a raw pointer
// p deleted automatically when it goes out of scopeunique_ptr has exclusive ownership: it cannot be copied (copy is = delete), only moved. There is exactly one owner at any time. Zero overhead compared to a raw pointer used correctly.
// Transfer ownership:
unique_ptr<Shape> owner = std::move(p); // p is now emptyUse for: single-owner heap objects, polymorphic objects stored in containers.
shared_ptr<T> — shared ownership
auto fp = make_shared<fstream>(name, mode); // reference count = 1
auto fp2 = fp; // reference count = 2; both point to same fstream
// When last shared_ptr is destroyed, reference count drops to 0 → object deletedshared_ptr uses reference counting: it maintains a count of owners; the last one to be destroyed calls delete. It can be copied — each copy increments the count.
Cost: reference counting adds overhead (typically two cache lines for the control block). Use only when shared ownership is genuinely required.
weak_ptr<T>: a non-owning reference to a shared_ptr-managed object. Prevents reference cycles (which would cause memory leaks with shared_ptr). Must be converted to a shared_ptr before use:
weak_ptr<Node> w = shared_ptr_to_node; // doesn't increment count
if (auto locked = w.lock()) { // returns shared_ptr if still alive
locked->do_something();
}make_unique and make_shared
Prefer over direct new for two reasons:
- Safety: if construction throws, no leak occurs
- Efficiency:
make_sharedallocates the object and control block in one allocation
auto p = make_unique<Widget>(args...); // unique_ptr<Widget>
auto s = make_shared<Widget>(args...); // shared_ptr<Widget>Summary of ownership options
| Tool | Ownership | Cost | Use when |
|---|---|---|---|
| Stack object | Automatic, scope-based | None | Lifetime matches scope |
unique_ptr<T> | Single owner | None (vs raw pointer) | One owner, heap-allocated |
shared_ptr<T> | Multiple owners | Reference count + control block | Genuinely shared lifetime |
weak_ptr<T> | Non-owning observer | None | Prevent reference cycles |
Raw pointer T* | Unspecified | None | Non-owning reference only |
Raw new/delete | Manual | None (but error-prone) | Avoid in application code |
In practice (Unreal Engine context)
Unreal Engine has its own memory management layered on top of C++:
UObject-derived classes are managed by Unreal’s garbage collector — not by C++ RAIITSharedPtr<T>/TUniquePtr<T>are Unreal’s equivalents ofshared_ptr/unique_ptrfor non-UObjecttypesUPROPERTY()on aUObject*member registers it with the GC to prevent premature collection- Plain C++ classes and structs (not derived from
UObject) should use standard RAII and smart pointers
Understanding which category an object belongs to — GC-managed vs. RAII-managed — is essential for avoiding memory bugs in Unreal code.
Open questions
shared_ptr’s reference count is thread-safe but the pointed-to object is not. In multithreaded game code (physics, AI), what additional synchronisation is required when usingshared_ptr?- Unreal’s garbage collector relies on
UPROPERTYannotations. What happens when aUObject*is stored withoutUPROPERTY? (Answer: it may be collected prematurely — a dangling pointer bug.)
Related
- cpp-basics — scope, lifetime, value semantics context for RAII
- cpp-classes-and-oop — constructors, destructors, virtual destructors
- cpp-templates —
unique_ptr<T>andshared_ptr<T>are template types - source-a-tour-of-cpp