In my previous articles, Beyond new and delete: A Practical Guide to Refactoring Raw Pointers to Smart Pointers and Beyond new and delete: to Weak Pointer, I explained how to refactor raw pointers into unique_ptr, shared_ptr, weak_ptr, and references.
But swapping pointer types is only half the battle. A successful refactoring also requires a systematic engineering process — one that combines ownership analysis, modern C++ types, static analysis, and incremental testing to ensure safety and correctness.
In this article, we focus on that engineering workflow:
Audit & Annotate → Static Analysis → Replace → Refactor Incrementally
| Step | Action |
|---|---|
| 1 | Audit & Annotate – Mark owning vs observer pointers, use GSL |
| 2 |
Static Analysis – Enable -Wall, run clang-tidy, add sanitizers |
| 3 |
Replace with Modern Types – unique_ptr, shared_ptr, span, references |
| 4 |
Refactor Incrementally – Start with leaf modules, remove manual delete
|
Step 1. Audit and Annotate Ownership
Goal: Make ownership explicit before changing any code.
Use comments or annotations to mark which pointers own memory and which do not.
Consider using the Guidelines Support Library (GSL) – available via vcpkg, Conan, or as a header‑only library (#include <gsl/gsl>):
-
gsl::owner<T*>for owning pointers. -
T*orgsl::not_null<T*>for non-owning pointers.
Example:
#include <gsl/gsl>
void process(gsl::not_null<MyClass*> ptr) {
// ptr is guaranteed non-null - no need to check!
ptr->doSomething();
}
void legacy_owner(gsl::owner<MyClass*> owned) {
// This raw pointer OWNS the memory
delete owned;
}
Step 2. Use Static Analysis and Compiler Options
Enable compiler warnings for unsafe pointer usage:
GCC/Clang:
-Wall -Wextra -WerrorMSVC:
/W4
or Use static analysis tools to catch raw pointer misuse:
clang-tidy
Cppcheck
# Run clang-tidy with Core Guidelines checks
clang-tidy --checks='cppcoreguidelines-owning-memory,modernize-*' file.cpp
Other useful tools: LeakSanitizer, Valgrind, Dr. Memory.
Typical warnings are listed below.
Warning 1: Missing 'gsl::owner' on allocated pointer
int* data = new int[100]; // warning here
delete[] data;
warning: initializing non-owner 'int *' with a newly created 'gsl::owner<>' [cppcoreguidelines-owning-memory]
Fix: gsl::owner<int*> data = new int[100]; or use std::unique_ptr.
Warning 2: Deleting a non-owner pointer
int* ptr = new int(42); // missing owner annotation
delete ptr; // warning here
warning: deleting a pointer through a type that is not marked 'gsl::owner<>' [cppcoreguidelines-owning-memory]
Fix: Mark with gsl::owner or use smart pointer.
Warning 3: Returning raw pointer from factory (ownership unclear)
int* make_int() { // warning: returning raw pointer
return new int(42);
}
warning: function returns a raw pointer that may be an owner [cppcoreguidelines-owning-memory]
Fix: std::unique_ptr<int> make_int() or mark as gsl::owner<int*>.
Warning 4: Assignment loses ownership
gsl::owner<int*> create() { return new int(42); }
void test() {
int* p = create(); // warning: owner assigned to non-owner
delete p;
}
warning: initializing non-owner 'int *' with a newly created 'gsl::owner<>' [cppcoreguidelines-owning-memory]
Fix: gsl::owner<int*> p = create(); or use auto p = create();.
Warning 5: Using std::unique_ptr with raw delete
auto p = std::make_unique<int>(42);
delete p.get(); // warning: manual delete on smart pointer
warning: 'delete' applied to pointer that is owned by a smart pointer [cppcoreguidelines-owning-memory]
Fix: Remove manual delete – the smart pointer handles it.
Step 3. Replace with Modern Types
| Ownership/Usage | Modern C++ Type | Notes |
|---|---|---|
| Sole ownership | std::unique_ptr<T> |
Use std::make_unique<T>()
|
| Shared ownership | std::shared_ptr<T> |
Use std::make_shared<T>()
|
| Non-owning, never null |
T& or gsl::not_null<T*>
|
Prefer references when possible |
| Non-owning, may be null |
T* (with optional GSL annotation) |
Raw pointer is fine as an observer |
| Array/view, non-owning | std::span<T> |
C++20 (or GSL span for older standards) |
Example:
void process(std::span<int> data); // Non-owning view over array/vector
💡 For dynamic arrays, consider std::vector<T> over std::unique_ptr<T[]> unless you need a specific allocator or C API compatibility.
Step 4. Refactor Incrementally
Start with leaf modules (fewest dependencies).
- Replace
gsl::owner<T*>withstd::unique_ptr<T>orstd::shared_ptr<T>as appropriate. - Use
std::span<T>for functions that take raw array pointers and sizes. - Update function signatures and member variables.
- Remove manual
deletecalls.
An Example: Step‑by‑Step Refactoring
Before:
void foo(int* arr, size_t n); // arr is not owned
void bar(gsl::owner<MyClass*> obj); // obj is owned
void caller() {
int* data = new int[10];
foo(data, 10);
delete[] data;
MyClass* mc = new MyClass();
bar(mc);
delete mc;
}
After:
#include <memory>
#include <span>
void foo(std::span<int> arr); // Non-owning view
void bar(std::unique_ptr<MyClass> obj); // Ownership transferred
void caller() {
auto data = std::make_unique<int[]>(10);
foo(std::span<int>(data.get(), 10));
// No delete needed
auto mc = std::make_unique<MyClass>();
bar(std::move(mc));
// No delete needed
}
When NOT to Refactor
Refactoring raw pointers isn't always the right move. Consider postponing or skipping when:
- Performance‑critical hot paths – the (small) overhead of smart pointers might matter (measure first).
- Codebases frozen for certification – aviation, medical, or safety‑critical systems.
- Interfacing with C libraries – raw pointers are often unavoidable there.
- The code is about to be deprecated – don't invest time in dead code.
Reference: clang-tidy Commands for Raw Pointer Refactoring
Check Commands (Analyze Only)
| Command | Purpose |
|---|---|
clang-tidy --checks='cppcoreguidelines-owning-memory' file.cpp |
Detect missing gsl::owner annotations and improper deletions |
clang-tidy --checks='modernize-make-unique' file.cpp |
Find raw new that can be replaced with std::make_unique
|
clang-tidy --checks='modernize-make-shared' file.cpp |
Find raw new that can be replaced with std::make_shared
|
clang-tidy --checks='modernize-use-auto' file.cpp |
Find verbose type names that can be replaced with auto
|
clang-tidy --checks='modernize-use-nullptr' file.cpp |
Find NULL or 0 that can be replaced with nullptr
|
clang-tidy --checks='cppcoreguidelines-owning-memory,modernize-*' file.cpp |
Run all relevant checks together |
Fix Commands (Apply Changes)
| Command | Purpose |
|---|---|
clang-tidy --fix --checks='modernize-make-unique' file.cpp |
Automatically replace new with std::make_unique
|
clang-tidy --fix --checks='modernize-make-shared' file.cpp |
Automatically replace new with std::make_shared
|
clang-tidy --fix --checks='modernize-use-auto' file.cpp |
Automatically replace verbose types with auto
|
clang-tidy --fix --checks='modernize-use-nullptr' file.cpp |
Automatically replace NULL with nullptr
|
clang-tidy --fix --checks='cppcoreguidelines-owning-memory' file.cpp |
Add gsl::owner annotations (manual review still recommended) |
Recommended Workflow with clang-tidy
# Step 1: Run checks to see what needs fixing
clang-tidy --checks='cppcoreguidelines-owning-memory,modernize-*' file.cpp
# Step 2: Apply automatic fixes safely
clang-tidy --fix --checks='modernize-make-unique,modernize-make-shared,modernize-use-auto,modernize-use-nullptr' file.cpp
# Step 3: Re-run checks to verify fixes
clang-tidy --checks='cppcoreguidelines-owning-memory' file.cpp
Conclusion
A professional refactoring from raw pointers to modern C++ involves:
- Explicitly annotating ownership (with GSL or comments)
- Using compiler warnings and static analysis
- Replacing with the right smart pointer or view type (
unique_ptr,shared_ptr,std::span, references) - Testing and reviewing at every step
This approach produces safer, more maintainable, and truly modern C++ code – without the sleepless nights hunting for memory leaks.
Top comments (0)