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
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:
- 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.
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.
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.
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.
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.
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.
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.
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.











