DEV Community

Wang - C++ Developer
Wang - C++ Developer

Posted on • Edited 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.


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.


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.


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.


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.


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_ptrsee the next article.


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.


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.


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.


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)