Modern C++ Memory Management: std::unique_ptr, std::shared_ptr & RAII
What You Will Master in This Tutorial
- Understand unique ownership with std::unique_ptr and zero-cost abstraction.
1. The Power of std::unique_ptr and Move Semantics
std::unique_ptr automatically deallocates memory when it goes out of scope.
#include <iostream>
#include <memory>
void runWorker() {
auto conn = std::make_unique<int>(42);
// Destructor called automatically when out of scope
}
Knowledge Check: Test Your Understanding
Frequently Asked Questions
2. Step by step: refactor a raw-pointer class to smart pointers
Theory becomes habit the first time you refactor a class that owns a heap object by hand. Here is the classic pre-modern shape β a Car that news an Engine and deletes it in the destructor:
class Engine {
public:
void start() {}
};
class Car {
Engine* engine; // owning raw pointer
public:
Car() : engine(new Engine) {}
~Car() { delete engine; } // manual cleanup
// Compiler-generated copy operations copy the POINTER,
// so two Cars delete the same Engine. Double free.
};
The bug is not the delete; it is that ownership is invisible. Nothing in the type says Car owns the Engine, so the compiler happily generates copy operations that duplicate the obligation. The refactor makes ownership the type:
#include <memory>
#include <utility>
class Engine {
public:
void start() {}
};
class Car {
std::unique_ptr<Engine> engine; // ownership is now visible
public:
Car() : engine(std::make_unique<Engine>()) {}
// unique_ptr is move-only, so Car is move-only too:
// copies are deleted by the compiler, moves transfer ownership.
Car(Car&&) noexcept = default;
Car& operator=(Car&&) noexcept = default;
Engine& getEngine() { return *engine; } // observer access, no ownership
};
Three rules finish the refactor anywhere you apply it. Factory functions return std::unique_ptr so the transfer of ownership is in the signature. Functions that only look at an object take a reference or a raw pointer obtained with .get() β observing is free and claims nothing. And a class that holds a unique_ptr almost never needs a handwritten destructor again: the Rule of Zero replaces the Rule of Five.
3. A complete worked example
A texture cache for a game is the canonical shared-ownership problem: many sprites share one loaded texture, the texture must unload when the last user lets go, and the cache itself must not keep everything alive forever. That combination β shared_ptr for users, weak_ptr for the cache β is the whole design:
#include <iostream>
#include <memory>
#include <string>
#include <unordered_map>
struct Texture {
std::string path;
explicit Texture(std::string p) : path(std::move(p)) {
std::cout << "Loaded " << path << "\n";
}
~Texture() { std::cout << "Unloaded " << path << "\n"; }
};
class TextureCache {
public:
std::shared_ptr<Texture> load(const std::string& path) {
if (auto it = cache_.find(path); it != cache_.end()) {
if (auto tex = it->second.lock()) { // still alive? reuse it
return tex;
}
}
auto tex = std::make_shared<Texture>(path);
cache_[path] = tex; // cache observes via weak_ptr
return tex;
}
private:
std::unordered_map<std::string, std::weak_ptr<Texture>> cache_;
};
int main() {
TextureCache cache;
{
auto hero = cache.load("hero.png"); // Loaded hero.png
auto heroAgain = cache.load("hero.png"); // reused, no reload
std::cout << "Same object: " << (hero == heroAgain) << "\n";
} // last shared_ptr dies -> Unloaded hero.png
auto bg = cache.load("bg.png"); // weak_ptr had expired -> fresh load
return 0;
}
Run it and the output teaches the semantics: hero.png loads once although load() was called twice, unloads exactly when the inner scope closes, and the cache never extends a textureβs life by a single instruction β because a weak_ptr observes without owning. Swap the cache to shared_ptr and every texture lives until the cache dies; that one-word difference is the most common real-world shared_ptr design error.
Step 3: hand ownership out of factory functions β and handle non-memory resources
Factories complete the pattern: a function that creates an object returns it as a unique_ptr, so the caller receives ownership without ever seeing a raw new. The same mechanism manages resources that are not memory at all. A C file handle has no destructor, so you supply one β a custom deleter β and RAII does the rest:
#include <cstdio>
#include <memory>
struct FileCloser {
void operator()(std::FILE* f) const {
if (f) std::fclose(f);
}
};
// A unique_ptr whose "delete" is fclose
using UniqueFile = std::unique_ptr<std::FILE, FileCloser>;
UniqueFile openLogFile(const char* path) {
return UniqueFile(std::fopen(path, "a")); // ownership moves to caller
}
void appendLine(const char* path, const char* line) {
auto file = openLogFile(path); // closes on every exit path,
if (!file) return; // including exceptions
std::fputs(line, file.get()); // .get(): observe, don't own
}
This is the deepest reason the deep-dive below tells you to design ownership first: once ownership is a type, files, sockets, and database handles all get the same one-line cleanup guarantee as heap objects β no finally blocks, no forgotten close on the error path.
4. Troubleshooting
- Compile error: "use of deleted function" when copying. You tried to copy a unique_ptr (or a class containing one, like the Car above). The compiler is enforcing single ownership. Pass the object by reference to share access, std::move it to transfer ownership, or β only if a second genuine owner exists β switch that member to shared_ptr deliberately.
- Crash at program exit, in a destructor, "double free or corruption". Two owners were built from the same raw pointer somewhere: two smart pointers constructed from one new, or a this pointer wrapped by two different managers. Find every place a raw pointer enters a smart pointer; in healthy code it happens exactly once per allocation, at the make_unique/make_shared call.
- LeakSanitizer reports memory "still reachable" at exit. A shared_ptr cycle: A holds B, B holds A, and neither count ever reaches zero. Rebuild with -fsanitize=address,leak, read the cycle in the report, and demote the back-reference to weak_ptr β the fix the deep-dive prescribes, now applied to a leak you can reproduce.
- Segfault when an observer outlives its owner. Something kept a raw pointer or reference (a registry, a callback, a UI widget) after the unique_ptr owner reset or went out of scope. The observer had no way to know the object died. Store a weak_ptr in the observer and lock() it at each use, so expiry is checkable instead of fatal.
Going deeper: smart pointers in practice
Knowing the syntax of std::unique_ptr and std::shared_ptr is only half the skill β the other half is developing instincts for ownership design. In production code, smart pointers shine when ownership is decided at the design stage: factory functions return std::unique_ptr to hand over ownership unambiguously, long-lived managers hold std::shared_ptr only for genuinely shared resources, and everything else is passed as a raw pointer or reference that simply observes. Teams that adopt this discipline find that entire classes of bugs β leaks, double frees, use-after-free β stop appearing in code review, because the type signatures themselves document who is responsible for every object, and the compiler enforces what the documentation used to merely promise. The result is code that is shorter and safer at the same time, because allocation and release stop being separate decisions scattered across a file.
Common mistakes
- Reaching for std::shared_ptr by default. Shared ownership hides design decisions and adds atomic reference-count overhead to every copy. Start with std::unique_ptr and upgrade only when a second owner truly exists.
- Wrapping the same raw pointer twice. Constructing two independent smart pointers from one raw pointer creates two owners that know nothing about each other β and the object gets deleted twice, with undefined behavior as the reward.
- Ownership cycles that never release. Two objects holding std::shared_ptr to each other keep both alive forever. Break the cycle by making the back-reference a std::weak_ptr.
- Taking smart pointers just to look. Functions that merely read an object should accept a raw pointer or a reference. Taking a std::shared_ptr by value bumps the reference count and advertises an ownership claim you never intended to make.
Frequently asked questions
Should I ever use new and delete directly in modern C++?
Almost never in application code. std::make_unique and std::make_shared build the smart pointer without a naked new in your code, which keeps ownership explicit and exception-safe from the very first line.
What is std::weak_ptr actually for?
A weak_ptr observes an object managed by shared_ptr without extending its lifetime. It is the standard fix for ownership cycles, and the right tool for caches and registries that must check whether an object still exists without keeping it alive.
Do smart pointers replace RAII, or are they RAII?
They are RAII applied to memory: the pointer object's destructor releases the resource automatically at scope exit, exactly the way a lock guard releases a mutex or a file wrapper closes its handle.