C++
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented C++
-
Section 4: Memory Management
-
Section 5: Templates and Generic Programming
-
Section 6: The Standard Template Library
-
Section 7: Modern C++ Features
-
Section 8: Error Handling
-
Section 9: Multithreading
-
Section 10: Operator Overloading and Type Conversion
-
Section 11: Advanced Topics
-
Section 12: Tooling and Build Systems
-
Section 13: Design Patterns in C++
-
Section 14: Interfacing with C and Systems Programming
-
Section 15: Networking and IPC Basics
-
Section 16: Graphics and Game Programming Basics
-
Section 17: The Boost Libraries
-
Section 18: Data Structures and Algorithms in C++
-
Section 19: Practical Projects
-
Section 20: More Concurrency Patterns
-
Section 21: More OOP and Design Practice
-
Section 22: File I/O and Streams
-
Section 23: More Standard Library
-
Section 24: Practice Exercises
-
Section 25: Interview and Algorithm Practice
-
Section 26: Compiler and Language Internals
-
Section 27: GUI and Application Frameworks Overview
-
Section 28: Testing and Quality Practices
-
Section 29: Numerics Library
-
Section 30: Concepts Library (C++20)
-
Section 31: Ranges Library (C++20) In Depth
-
Section 32: More Utility Library
47: Smart Pointers: unique_ptr
I've seen this exact pattern in countless code reviews over the years. A developer is managing a resource—maybe a network socket, a file handle, or a game object—and they're doing everything "by the book" with new and delete. It looks clean until you realize how fragile it actually is.
#include <iostream>
#include <string>
struct Player {
std::string name;
Player(std::string n) : name(n) { std::cout < "Player " < name < " created\n"; }
~Player() { std::cout < "Player " < name < " destroyed\n"; }
void shout() { std::cout < name < " screams!\n"; }
};
void gameLoop() {
Player* player = new Player("Hero");
bool gameOver = false;
if (/* some complex logic */ true) {
std::cout < "Unexpected error occurred!\n";
return; // Oops! We just leaked the Player object.
}
player->shout();
delete player;
}
int main() {
gameLoop();
return 0;
}
The Leak Hidden in Plain Sight
If you run the code above, you'll notice the "destroyed" message never prints. The bug is a classic memory leak. Because we returned early from the function due to an error, the delete player line was skipped. In a small example, this is a nuisance; in a AAA game engine or a high-frequency trading app, this is a critical failure that will eventually crash the system as it runs out of RAM.
You might think, "I'll just add another delete before the return statement." Sure, you could. But what if the function has ten different exit points? Or what if player->shout() throws an exception? You can't possibly wrap every single line in a try-catch block just to ensure a pointer gets deleted. It's tedious and error-prone.
Automating Ownership with unique_ptr
This is where std::unique_ptr comes in. Think of it as a wrapper that "owns" the object it points to. When the unique_ptr itself goes out of scope (whether through a return statement, the end of a function, or an exception), it automatically calls delete for you. It leverages a concept called RAII (Resource Acquisition Is Initialization).
Here is how I would rewrite that gameLoop function to be bulletproof:
#include <iostream>
#include <memory> // Required for smart pointers
void gameLoop() {
// Use make_unique instead of 'new'
auto player = std::make_unique<Player>("Hero");
bool gameOver = false;
if (true) {
std::cout < "Unexpected error occurred!\n";
return; // No leak! player is destroyed automatically here.
}
player->shout();
// No delete needed.
}
By using std::make_unique, we've tied the lifetime of the Player object to the lifetime of the player variable. The moment the function exits, the wrapper is destroyed, and it takes the Player object down with it. It's clean, it's safe, and it's the modern C++ way.
You Can't Just Copy Ownership
There is one big catch with unique_ptr: as the name implies, it is unique. You cannot copy it. If you try to pass it into another function by value or assign it to another unique_ptr, the compiler will scream at you.
I remember a junior dev spending an hour fighting the compiler because they tried to put a unique_ptr into a std::vector and then tried to copy that vector. The compiler was actually saving them from a disaster: if two unique_ptrs owned the same memory, they would both try to delete it when they died, leading to a double-free crash.
If you actually want to move ownership from one place to another, you have to be explicit using std::move:
std::unique_ptr<Player> p1 = std::make_unique<Player>("Hero");
// std::unique_ptr<Player> p2 = p1; // This would fail to compile!
std::unique_ptr<Player> p2 = std::move(p1);
// Now p2 owns the player, and p1 is null.
Essentially, std::move tells the compiler, "I'm done with this resource; give it to someone else."
📋 Practical Task
Building a Smart Command Buffer
You are building a simple command system for a text adventure game. Each command is a different class derived from a base Command class. To avoid memory leaks when commands are added to or cleared from the queue, you must use std::unique_ptr.
Your Task:
Implement a CommandBuffer class that manages a collection of commands. Your implementation should meet these requirements:
- Create a base class
Commandwith a virtual destructor and a virtual methodexecute(). - Create at least two derived classes (e.g.,
MoveCommandandAttackCommand) that overrideexecute()to print a specific message. - The
CommandBufferclass should contain astd::vector<std::unique_ptr<Command>>. - Implement a method
void addCommand(std::unique_ptr<Command> cmd)that adds a command to the buffer. - Implement a method
void runAll()that iterates through the buffer, callsexecute()on each command, and then clears the buffer.
Goal: Ensure that no matter when the CommandBuffer is destroyed or cleared, all allocated commands are properly deleted without using a single manual delete call.
There are no comments for now.