DEV Community

Rui-Tech
Rui-Tech

Posted on

Beyond new and delete: A Practical Guide to Refactoring Raw Pointers to Smart Pointers

You've been there. You inherit a codebase filled with Widget* w = new Widget(), manual delete calls scattered across error-prone destructors, and the occasional use-after-free that crashes in production once a week.

You know C++11's smart pointers are the answer. But refactoring a raw pointer to std::unique_ptr, std::shared_ptr, or even back to a plain reference isn't always obvious. The core question is: Who owns this object?

In this article, we'll build a decision framework. In 15 minutes, you'll learn how to mechanically refactor raw pointers and when to choose each smart pointer—or avoid them altogether.


Chapter 1: The Decision Workflow (15-Second Check)

Raw Pointer?
    │
    ├─ Does it OWN the object?
    │       │
    │       ├─ NO  → raw reference (&) or raw pointer (*)
    │       │        (never delete)
    │       │
    │       └─ YES → Is ownership EXCLUSIVE?
    │                   │
    │                   ├─ YES → std::unique_ptr ← 80% of cases
    │                   │
    │                   └─ NO  → Is ownership SHARED?
    │                               │
    │                               ├─ YES → Is there a CYCLE?
    │                               │           │
    │                               │           ├─ YES → std::weak_ptr
    │                               │           │
    │                               │           └─ NO  → std::shared_ptr
    │                               │
    │                               └─ NO  → Re-examine design
Enter fullscreen mode Exit fullscreen mode

The 3-Question Drill:

Question Answer → Action
Owns? No → Raw reference/pointer (never delete)
Exclusive? Yes → unique_ptr
Shared? Yes → shared_ptr (check for cycles → weak_ptr)

That's it. 80% of raw pointers become unique_ptr. 15% become raw references. 5% become shared_ptr/weak_ptr.


Chapter 2: Step 0 — The Golden Rule of Refactoring

Never change behavior while refactoring. Start with a single raw pointer. Ask three questions:

  1. Is this pointer nullable? (Can it be nullptr?)
  2. Who is responsible for deleting the object?
  3. Is ownership shared or unique?

Once you answer those, the path becomes clear.


Chapter 3: The Obvious Case — Exclusive Ownership → std::unique_ptr

Use unique_ptr when exactly one part of the code owns the object, but the pointer might be moved or transferred.

Signs you need unique_ptr:

  • The raw pointer is deleted in the same class that creates it.
  • The pointer is never copied (only moved or passed as a raw pointer to functions that don't store it).
  • You see delete ptr; in a destructor and nowhere else.

Before refactoring:

class Connection {
public:
    Connection() : socket_(new Socket()) {}
    ~Connection() { delete socket_; }
    // Manual move/copy? Omitted for brevity — disaster waiting.
private:
    Socket* socket_;
};
Enter fullscreen mode Exit fullscreen mode

After refactoring:

class Connection {
public:
    Connection() : socket_(std::make_unique<Socket>()) {}
    // Destructor auto-deletes. Move is automatic.
    Connection(Connection&&) = default;
private:
    std::unique_ptr<Socket> socket_;
};
Enter fullscreen mode Exit fullscreen mode

Key nuance: You can still pass a raw pointer to a function that doesn't own the object (e.g., void send(Socket* s)). Use .get() for that. But never .release() unless you truly mean to transfer ownership.

Heuristic: If you would have used std::auto_ptr (RIP), use unique_ptr.


Chapter 4: Shared Ownership → std::shared_ptr

Use shared_ptr when multiple parts of the system need to keep the object alive and you cannot predict which one will outlive the others.

Signs you need shared_ptr:

  • The pointer is stored in several containers or callbacks.
  • You have std::vector and multiple threads or components delete items unpredictably.
  • You find yourself implementing reference counting manually (e.g., AddRef/Release).

Before refactoring:

class Node {
public:
    Node* parent;
    std::vector<Node*> children;
    ~Node() {
        for (auto* child : children) delete child;
    }
};
Enter fullscreen mode Exit fullscreen mode

This leaks if a child is referenced from two parents. Don't do this.

After refactoring:

class Node : public std::enable_shared_from_this<Node> {
public:
    std::shared_ptr<Node> parent;
    std::vector<std::shared_ptr<Node>> children;
    // No manual destructor.
};
Enter fullscreen mode Exit fullscreen mode

The cost:
shared_ptr doubles the size (two pointers: object + control block) and uses atomic increments (slower, but fine for most apps). Only reach for shared_ptr when unique_ptr is impossible.


Chapter 5: The Observer Case — Breaking Cycles → std::weak_ptr

shared_ptr has a fatal flaw: cyclic references. If two shared_ptrs point to each other, they never die.

The problem:

class Person {
    std::shared_ptr<Person> mother;
    std::shared_ptr<Person> father;  // Cycle!
};
Enter fullscreen mode Exit fullscreen mode

The fix:

class Person {
    std::shared_ptr<Person> mother;  // Owning
    std::weak_ptr<Person> father;    // Observing (no cycle)
};
Enter fullscreen mode Exit fullscreen mode

Use weak_ptr when you need a non-owning reference to an object managed by shared_ptr.

Full explanation and refactoring techniques for weak_ptr → see the next article.


Chapter 6: The No-Ownership Case — Raw References & Raw Pointers

Smart pointers express ownership. Not every pointer needs to be smart. In fact, using a shared_ptr for every single pointer is an anti-pattern (slow, bloated, semantically wrong).

Use plain references (&) or raw pointers (*) for non-owning observers when you are certain the lifetime is longer than the use.

When to use a raw reference:

  • The object must exist (no nullptr).
  • You are not storing it for later (e.g., function parameter).
  • Example: void draw(const Shape& shape);

When to use a raw pointer:

  • The observer can be null.
  • You need to reseat it (references can't be reassigned).
  • Example: void setLogger(Logger* logger) { logger_ = logger; } – but only if the Logger outlives this object.

The critical rule:
A raw pointer or reference must never be deleted. If you see delete ptr on a raw pointer, you made a wrong turn.


Chapter 7: Real Refactoring Example — A Cache System

Let's refactor a small in-memory cache from raw pointers to smart pointers.

Before (broken):

class Cache {
    std::unordered_map<std::string, Data*> store;
public:
    void put(const std::string& key, Data* data) { store[key] = data; }
    Data* get(const std::string& key) { return store[key]; }
    ~Cache() { for (auto& [k, v] : store) delete v; }
};
// Usage: who deletes the Data? Cache does, but what if two caches share it?
Enter fullscreen mode Exit fullscreen mode

After (clear ownership):

class Cache {
    // Cache owns the Data exclusively.
    std::unordered_map<std::string, std::unique_ptr<Data>> store;
public:
    void put(const std::string& key, std::unique_ptr<Data> data) {
        store[key] = std::move(data);
    }
    // Returns non-owning observer
    Data* get(const std::string& key) {
        auto it = store.find(key);
        return (it != store.end()) ? it->second.get() : nullptr;
    }
};
Enter fullscreen mode Exit fullscreen mode

Now ownership is explicit. The cache destroys the data. Callers can observe but not delete.


Chapter 8: Common Pitfalls During Refactoring

Mixing ownership styles
std::shared_ptr<Widget> sp = std::make_unique<Widget>(); // Works but confusing. Prefer consistent smart pointer types.

Using shared_ptr for everything "just to be safe"
This hides design issues and kills performance (atomic ops). Refactor to unique_ptr first.

Calling .get() and storing the raw pointer long-term
That's just raw pointer ownership again. Store the smart pointer or use weak_ptr.

Forgetting make_unique / make_shared

// Old: std::unique_ptr<Widget> p(new Widget());
// New:
auto p = std::make_unique<Widget>();
Enter fullscreen mode Exit fullscreen mode

make_unique is exception-safe and slightly more efficient.


Chapter 9: When Not to Refactor

Some raw pointers are fine:

  • Non-owning iterators into a buffer.
  • Polymorphic casts in performance-critical loops (you still need to ensure lifetime).
  • Legacy APIs that expect raw pointers and manage their own memory. Don't force shared_ptr into a C-style callback.

In those cases, document the lifetime contract: "This pointer is valid only during the call."


Conclusion: Ownership Is Documentation

Refactoring raw pointers to smart pointers isn't just about memory safety—it's about expressing intent. When I see unique_ptr, I know: This object has a single, clear owner. When I see shared_ptr, I know: Multiple agents collaborate here. When I see a raw reference, I know: I'm just visiting; don't delete.

Next time you look at a MyClass* member variable, ask: "Who owns this?" Then pick the tool that answers that question in code.

Your future self—and your teammates—will thank you.


Top comments (0)