DEV Community

Rui-Tech
Rui-Tech

Posted on

Beyond New and Delete: Engineering Approach with gsl::owner, std::span and clang-tidy

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 & AnnotateStatic AnalysisReplaceRefactor 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 Typesunique_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* or gsl::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;
}
Enter fullscreen mode Exit fullscreen mode

Step 2. Use Static Analysis and Compiler Options

Enable compiler warnings for unsafe pointer usage:

  • GCC/Clang: -Wall -Wextra -Werror

  • MSVC: /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 
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode
warning: initializing non-owner 'int *' with a newly created 'gsl::owner<>' [cppcoreguidelines-owning-memory]
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
warning: deleting a pointer through a type that is not marked 'gsl::owner<>' [cppcoreguidelines-owning-memory]
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode
warning: function returns a raw pointer that may be an owner [cppcoreguidelines-owning-memory]
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode
warning: initializing non-owner 'int *' with a newly created 'gsl::owner<>' [cppcoreguidelines-owning-memory]
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
warning: 'delete' applied to pointer that is owned by a smart pointer [cppcoreguidelines-owning-memory]
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

💡 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*> with std::unique_ptr<T> or std::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 delete calls.

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;
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)