Skip to Content
Course content

53: Practice Exercise: Refactoring Raw Pointers to Smart Pointers

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

Think about the difference between borrowing a physical book from a friend and using a digital library subscription. With the physical book, the responsibility is entirely on you. You have to remember to bring it back, or it's gone from your friend's shelf forever—and if you lose it, that's a permanent loss (a "leak" in their collection). But with a digital subscription, the system handles the access. Once your subscription expires or you click "return," the system revokes your access automatically. You don't have to remember to "delete" your access to the book; the system's logic takes care of it for you.

In C++, raw pointers are those physical books. You call new to get a resource, and you must remember to call delete. If you forget, or if your code throws an exception before it hits that delete line, you've got a memory leak. Smart pointers are the digital subscription. They wrap the raw pointer in an object that knows exactly when it's no longer needed and cleans up the memory automatically.

The Fragility of Manual Cleanup

I've spent way too many late nights hunting down memory leaks in legacy codebases that looked like this. Let's look at a simple SessionManager that handles user connections. In the old way, we'd do something like this:


class UserSession {
public:
    UserSession(int id) : sessionId(id) {}
    ~UserSession() { /* cleanup resources */ }
    void process() { /* logic here */ }
private:
    int sessionId;
};

class SessionManager {
    UserSession* currentSession; 
public:
    void startSession(int id) {
        if (currentSession) delete currentSession; // Manual cleanup
        currentSession = new UserSession(id);
    }
    ~SessionManager() {
        delete currentSession; // If you forget this, you leak!
    }
};


This looks fine on the surface, but it's brittle. If startSession throws an exception between the delete and the new, or if someone adds a return statement halfway through the function, you're in trouble. You're manually managing a lifecycle, and humans are notoriously bad at that.

Switching to Exclusive Ownership

Most of the time, a resource has one clear owner. In the example above, the SessionManager owns the UserSession. This is the perfect use case for std::unique_ptr. It ensures that there is exactly one pointer to the memory, and when that pointer goes out of scope, the memory is freed immediately.

Here is how I would refactor that same code:


#include <memory>

class SessionManager {
    std::unique_ptr<UserSession> currentSession; 
public:
    void startSession(int id) {
        // No manual delete needed! 
        // assign replaces the old object and deletes it automatically.
        currentSession = std::make_unique<UserSession>(id);
    }
    // No destructor needed! unique_ptr cleans itself up.
};


Notice how much noise disappeared. I don't have to write a destructor. I don't have to remember to check if currentSession exists before replacing it. std::make_unique (introduced in C++14) is the gold standard here because it's more efficient and safer than calling new inside the constructor.

Handling Shared Resources

Now, what if multiple parts of your program need to keep a resource alive, but you don't know who will be the last one to finish using it? Imagine a Texture in a game engine used by twenty different Sprite objects. You can't use unique_ptr because there isn't one single owner.

This is where std::shared_ptr comes in. It uses "reference counting." Every time you copy the pointer, a counter goes up. Every time a pointer is destroyed, the counter goes down. When the counter hits zero, the memory is finally deleted.


class Texture { /* ... */ };

class Sprite {
    std::shared_ptr<Texture> texture;
public:
    Sprite(std::shared_ptr<Texture> tex) : texture(tex) {}
};

// Usage
auto sharedTex = std::make_shared<Texture>();
Sprite s1(sharedTex); 
Sprite s2(sharedTex); 
// The texture stays alive as long as s1, s2, or sharedTex exist.


A quick word of caution: don't use shared_ptr just because you're lazy. Reference counting has a small performance overhead. Always start with unique_ptr. Only "upgrade" to shared_ptr when you actually have multiple owners. If you find yourself with two shared_ptrs pointing at each other, you've created a circular reference, and neither will ever be deleted. That's when you'll need std::weak_ptr, but we'll save that for another time.




📋 Practical Task

Refactoring the Legacy Particle System

You've been handed a legacy ParticleEmitter class. It currently uses a raw array of pointers to Particle objects. The current implementation is leaking memory because the destructor is incorrectly implemented, and it doesn't handle mid-loop errors.

Your Task: Refactor the following code to eliminate all raw new and delete calls. Use std::unique_ptr and std::vector to ensure the memory is managed automatically.


#include <iostream>
#include <vector>

class Particle {
public:
    Particle(float x, float y) : x(x), y(y) {
        std::cout < "Particle created\n";
    }
    ~Particle() {
        std::cout < "Particle destroyed\n";
    }
private:
    float x, y;
};

class ParticleEmitter {
    Particle** particles; // Raw array of raw pointers
    int count;

public:
    ParticleEmitter(int n) : count(n) {
        particles = new Particle*[n];
        for (int i = 0; i < n; ++i) {
            particles[i] = new Particle(0.0f, 0.0f);
        }
    }

    ~ParticleEmitter() {
        // BUG: The original developer forgot to loop through 
        // and delete individual particles before deleting the array!
        delete[] particles; 
    }
};

int main() {
    {
        ParticleEmitter emitter(5);
    } // Emitter goes out of scope here
    return 0;
}


Requirements:

  • Replace Particle** particles with a std::vector<std::unique_ptr<Particle>>.
  • Replace the new` calls in the constructor with std::make_unique.
  • Remove the manual delete[] from the destructor (or remove the destructor entirely if it's no longer needed).
  • Verify that "Particle destroyed" is printed 5 times when the program ends.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.