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
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:
- Is this pointer nullable? (Can it be
nullptr?) - Who is responsible for deleting the object?
- 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_;
};
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_;
};
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::vectorand 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;
}
};
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.
};
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!
};
The fix:
class Person {
std::shared_ptr<Person> mother; // Owning
std::weak_ptr<Person> father; // Observing (no cycle)
};
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 theLoggeroutlives 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?
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;
}
};
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>();
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_ptrinto 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)