Skip to Content
Course content

47: Smart Pointers: unique_ptr

Click on the "Edit" button in the top corner of the screen to edit your slide content.

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 Command with a virtual destructor and a virtual method execute().
  • Create at least two derived classes (e.g., MoveCommand and AttackCommand) that override execute() to print a specific message.
  • The CommandBuffer class should contain a std::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, calls execute() 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.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.