Lesson 10 of 13
RAII, Smart Pointers & Move Semantics
Learn RAII, smart and move semantics: how C++ ties memory, files and locks to an object's lifetime so they are released automatically, even when an is thrown.
#include <fstream>
#include <mutex>
#include <iostream>
// RAII: acquire in the constructor, release in the destructor.
class FileLogger {
std::ofstream out;
public:
explicit FileLogger(const std::string& path) : out(path) {
if (!out) throw std::runtime_error("cannot open " + path);
}
~FileLogger() { out.flush(); } // runs no matter how we leave the scope
void log(const std::string& msg) { out << msg << '\n'; }
};
void work() {
FileLogger logger("app.log");
logger.log("started");
throw std::runtime_error("boom"); // the file is STILL closed properly
}
std::mutex m;
void guarded() {
std::lock_guard<std::mutex> lock(m); // locks here
// ... critical section ...
} // unlocks here, even on exception#include <memory>
#include <vector>
#include <iostream>
struct Node {
int value;
std::unique_ptr<Node> next; // owns the rest of the list
explicit Node(int v) : value(v) {}
~Node() { std::cout << "destroying " << value << "\n"; }
};
int main() {
// unique_ptr: exactly one owner. Zero overhead vs a raw pointer.
auto a = std::make_unique<Node>(1);
a->next = std::make_unique<Node>(2);
// auto copy = a; // compile error — unique_ptr cannot be copied
auto moved = std::move(a); // ownership transferred; a is now nullptr
std::cout << (a == nullptr) << "\n"; // 1
// shared_ptr: reference counted, freed when the last owner dies.
auto s1 = std::make_shared<Node>(10);
{
auto s2 = s1;
std::cout << s1.use_count() << "\n"; // 2
}
std::cout << s1.use_count() << "\n"; // 1
// weak_ptr: observes without owning — breaks reference cycles.
std::weak_ptr<Node> observer = s1;
if (auto locked = observer.lock()) std::cout << locked->value << "\n";
} // everything freed here, in reverse order of construction#include <vector>
#include <string>
#include <utility>
// Move semantics: transfer resources instead of copying them.
class Buffer {
std::size_t size_;
int* data_;
public:
explicit Buffer(std::size_t n) : size_(n), data_(new int[n]{}) {}
~Buffer() { delete[] data_; }
// Copy constructor — expensive: allocates and copies
Buffer(const Buffer& other) : size_(other.size_), data_(new int[other.size_]) {
std::copy(other.data_, other.data_ + size_, data_);
}
// Move constructor — cheap: steals the pointer, leaves the source empty
Buffer(Buffer&& other) noexcept : size_(other.size_), data_(other.data_) {
other.data_ = nullptr;
other.size_ = 0;
}
Buffer& operator=(Buffer&& other) noexcept {
if (this != &other) {
delete[] data_;
data_ = other.data_; size_ = other.size_;
other.data_ = nullptr; other.size_ = 0;
}
return *this;
}
};
void demo() {
std::vector<std::string> names;
std::string big(1'000'000, 'x');
names.push_back(big); // copies one million characters
names.push_back(std::move(big)); // steals them; big is now empty but valid
}
// Rule of Zero: if your class holds only vectors, strings and smart pointers,
// write NO destructor, copy or move operations — the compiler gets them right.Common question
Does modern C++ still need manual delete?
RAII ties a resource's lifetime to an object's . The constructor acquires; the destructor releases; the language guarantees the destructor runs on every way out of the scope, including exceptions. Smart pointers apply this to heap memory, so ownership shows up in the type instead of in .
Modern practice: prefer plain values and containers to raw allocation, use make_unique and make_shared instead of new, use raw pointers or references only as that do not own anything, and write classes that either manage one resource properly or manage at all.
Source: The C++ Core Guidelines, rule R.1: "Manage resources automatically using resource handles and RAII (Resource Acquisition Is Initialization)."
Common mix-ups
Each of these sounds reasonable. Think about why it is wrong, then open the explanation.
C++ has no garbage collector, so every new needs a delete written by hand.
RAII is only a technique for memory.
shared_ptr is the safe default because it never frees too early.
Tip: use the ← and → keys to move between lessons.