Last updated: March 22, 2026

Modernizing legacy C++ code is one of the highest-value AI use cases in systems programming. C++11 through C++23 introduced dozens of improvements over C++03: smart pointers, move semantics, auto, lambdas, ranges, concepts, and more. Manually applying these refactors is tedious and error-prone. AI tools can identify modernization opportunities and generate safe rewrites.

What “Modern C++” Refactoring Covers

The most impactful transformations:

  1. Raw pointers → smart pointers (new/deletestd::unique_ptr/shared_ptr)
  2. C-style arrays → std::array/std::vector/std::span
  3. Manual loops → range-based for / std::ranges
  4. NULLnullptr
  5. Manual RAII → scope guards / move semantics
  6. std::string + C-style string mixing → modern string handling
  7. typedefusing
  8. C-style casts → static_cast/reinterpret_cast
  9. Mutex + bool flag → std::atomic
  10. C++17 if constexpr, std::optional, std::variant

Task 1: Smart Pointer Conversion

Prompt: “Modernize this C++ class to use smart pointers instead of raw pointer member variables. Add appropriate move semantics.”

Legacy code:

// Legacy C++03 style
class ResourceManager {
public:
    ResourceManager(const char* config_path) {
        config_ = new Config(config_path);
        buffer_ = new char[BUFFER_SIZE];
        logger_ = new Logger();
    }

    ~ResourceManager() {
        delete config_;
        delete[] buffer_;
        delete logger_;
    }

    // Copy constructor and operator= intentionally not defined (memory issues if copied)

private:
    Config* config_;
    char* buffer_;
    Logger* logger_;
    static const int BUFFER_SIZE = 4096;
};

Claude’s modernized version:

// Modern C++17 style
#include <memory>
#include <array>
#include <string_view>

class ResourceManager {
public:
    explicit ResourceManager(std::string_view config_path)
        : config_(std::make_unique<Config>(config_path))
        , buffer_(std::make_unique<std::array<char, BUFFER_SIZE>>())
        , logger_(std::make_unique<Logger>())
    {}

    // Rule of Zero: unique_ptr handles destruction automatically
    // Delete copy — ResourceManager is move-only
    ResourceManager(const ResourceManager&) = delete;
    ResourceManager& operator=(const ResourceManager&) = delete;

    // Default move is correct — unique_ptr is movable
    ResourceManager(ResourceManager&&) noexcept = default;
    ResourceManager& operator=(ResourceManager&&) noexcept = default;

    ~ResourceManager() = default;  // Explicit for clarity

private:
    static constexpr std::size_t BUFFER_SIZE = 4096;

    std::unique_ptr<Config> config_;
    std::unique_ptr<std::array<char, BUFFER_SIZE>> buffer_;
    std::unique_ptr<Logger> logger_;
};

Claude applies the Rule of Zero (let smart pointers handle destruction), uses make_unique instead of new, changes const char* to std::string_view, adds explicit move semantics with = default, and converts static const int to static constexpr std::size_t.

GPT-4 produces similar output but sometimes forgets noexcept on the move constructor, which prevents the class from being used in std::vector efficiently (vectors only use move semantics if move constructor is noexcept).

Task 2: Algorithm Modernization

Legacy code:

// C++03: manual sort and search
std::vector<Employee> employees = getEmployees();

// Sort by salary descending
std::sort(employees.begin(), employees.end(),
    [](const Employee& a, const Employee& b) {
        return a.salary > b.salary;
    });

// Find all employees in engineering dept
std::vector<Employee> engineers;
for (std::vector<Employee>::iterator it = employees.begin();
     it != employees.end(); ++it) {
    if (it->department == "Engineering") {
        engineers.push_back(*it);
    }
}

// Find highest paid engineer
Employee* top_engineer = nullptr;
for (size_t i = 0; i < engineers.size(); ++i) {
    if (!top_engineer || engineers[i].salary > top_engineer->salary) {
        top_engineer = &engineers[i];
    }
}

Claude’s C++20 refactor:

#include <algorithm>
#include <ranges>
#include <optional>

auto employees = getEmployees();

// C++20 ranges — composable, lazy, readable
auto top_engineer = employees
    | std::views::filter([](const Employee& e) {
        return e.department == "Engineering";
      })
    | std::views::transform([](const Employee& e) -> const Employee& { return e; })
    | std::ranges::max_element([](const Employee& a, const Employee& b) {
        return a.salary < b.salary;
      });

// top_engineer is an iterator; check if we found one
if (top_engineer != std::ranges::end(
        employees | std::views::filter([](const Employee& e) {
            return e.department == "Engineering";
        })
    ))
{
    std::println("Top engineer: {} (${:.2f})", top_engineer->name, top_engineer->salary);
}

Claude also provides the cleaner two-step version using std::optional:

// Cleaner two-step approach (Claude's preferred refactor)
auto engineers_view = employees | std::views::filter(
    [](const Employee& e) { return e.department == "Engineering"; }
);

auto it = std::ranges::max_element(engineers_view,
    [](const Employee& a, const Employee& b) { return a.salary < b.salary; });

if (it != engineers_view.end()) {
    std::println("Top engineer: {} salary: ${:.2f}", it->name, it->salary);
}

Task 3: Thread Safety Modernization

Legacy pattern (racy):

// C++03: non-atomic flag — undefined behavior with multiple threads
class Singleton {
public:
    static Singleton* getInstance() {
        if (!instance_) {           // Race condition: two threads can both see null
            instance_ = new Singleton();
        }
        return instance_;
    }

private:
    static Singleton* instance_;
};

Claude’s modern thread-safe version:

// C++11+: Meyers Singleton — thread-safe by the standard
class Singleton {
public:
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

    static Singleton& getInstance() {
        static Singleton instance;  // Thread-safe initialization since C++11
        return instance;
    }

    // If you need pointer semantics:
    static Singleton* getInstancePtr() {
        return &getInstance();
    }

private:
    Singleton() = default;
};

// For genuinely shared state, prefer std::atomic with modern patterns
std::atomic<int> request_count{0};
std::atomic<bool> shutdown_requested{false};

// Instead of:
// bool shutdown_requested = false;  // + mutex

Task 4: Error Handling with std::expected (C++23)

// Generated by Claude for C++23 codebases
#include <expected>
#include <system_error>
#include <fstream>

enum class FileError { NotFound, PermissionDenied, ReadError };

std::expected<std::string, FileError> readFile(const std::filesystem::path& path) {
    if (!std::filesystem::exists(path)) {
        return std::unexpected(FileError::NotFound);
    }

    std::ifstream file(path);
    if (!file.is_open()) {
        return std::unexpected(FileError::PermissionDenied);
    }

    std::string content((std::istreambuf_iterator<char>(file)),
                         std::istreambuf_iterator<char>());

    if (file.fail() && !file.eof()) {
        return std::unexpected(FileError::ReadError);
    }

    return content;
}

// Usage — no exceptions, no output parameters
auto result = readFile("/etc/config.json");
if (result) {
    processConfig(*result);
} else {
    switch (result.error()) {
        case FileError::NotFound:      handleMissing(); break;
        case FileError::PermissionDenied: handlePermission(); break;
        case FileError::ReadError:     handleReadError(); break;
    }
}

Task 5: Template Modernization with Concepts (C++20)

One area where AI tools add serious value is converting unconstrained templates to concept-constrained templates. Unconstrained templates produce notoriously unreadable compiler errors; concepts make errors actionable.

Legacy unconstrained template:

// C++14: no constraints — bad error messages if T is wrong
template<typename T>
T clamp(T value, T lo, T hi) {
    return value < lo ? lo : (value > hi ? hi : value);
}

Claude’s C++20 concepts version:

#include <concepts>

// C++20: constrained — clear error if T doesn't support < and >
template<std::totally_ordered T>
T clamp(T value, T lo, T hi) {
    return value < lo ? lo : (value > hi ? hi : value);
}

// Or, for maximum clarity, use a requires clause:
template<typename T>
    requires std::totally_ordered<T> && std::copyable<T>
T clamp(T value, T lo, T hi) {
    return value < lo ? lo : (value > hi ? hi : value);
}

For more complex cases, Claude generates custom concepts:

// Custom concept: anything that behaves like a container
template<typename T>
concept Container = requires(T c) {
    { c.begin() } -> std::input_or_output_iterator;
    { c.end() }   -> std::input_or_output_iterator;
    { c.size() }  -> std::convertible_to<std::size_t>;
    typename T::value_type;
};

// Now usable as a constraint
template<Container C>
void printAll(const C& container) {
    for (const auto& item : container) {
        std::cout << item << '\n';
    }
}

GPT-4 handles concepts syntax correctly, but tends to suggest weaker constraints (like std::regular when std::totally_ordered is more precise). Copilot requires good context in the file — if your surrounding code doesn’t use concepts, it defaults to unconstrained templates.

Task 6: Modernizing typedef and Type Aliases

This is a quick win that AI tools handle consistently well:

// Legacy C++03 typedef style
typedef std::map<std::string, std::vector<int>> StringToIntVec;
typedef void (*CallbackFn)(int, const char*);
typedef unsigned long long u64;

// Modern using style — generated by all three tools
using StringToIntVec = std::map<std::string, std::vector<int>>;
using CallbackFn = void(*)(int, const char*);
using u64 = unsigned long long;

// using also works for templates (typedef cannot):
template<typename T>
using Vec2D = std::vector<std::vector<T>>;

The key advantage of using over typedef is template alias support. typedef cannot template aliases — using can. All three AI tools know this distinction, but only Claude proactively mentions why using is preferred when explaining the refactor.

Task 7: Structured Bindings and if Initializers (C++17)

C++17 introduced two quality-of-life features that AI tools apply well when asked to modernize code that uses maps or error codes:

// Legacy: verbose map iteration
std::map<std::string, int> scores;
for (auto it = scores.begin(); it != scores.end(); ++it) {
    std::string key = it->first;
    int value = it->second;
    process(key, value);
}

// Modern C++17: structured bindings
for (const auto& [key, value] : scores) {
    process(key, value);
}

// Legacy: checking map insertion result
auto result = scores.insert({"alice", 42});
if (!result.second) {
    // Already existed
    result.first->second = 42;  // Update
}

// Modern C++17: if initializer + structured binding
if (auto [it, inserted] = scores.insert({"alice", 42}); !inserted) {
    it->second = 42;  // Update existing
}

Claude applies structured bindings proactively when it sees paired .first/.second accesses. GPT-4 does the same. Copilot applies them inline as autocomplete when you type for (const auto& .

Prompting Strategy for Bulk Refactoring

When refactoring a large codebase, use a two-pass approach:

Pass 1: Audit prompt — identifies what needs modernizing without changing code:

Analyze this C++ source file and list every modernization opportunity
(C++11 through C++20). Group by type: smart pointers, algorithms, type aliases,
thread safety, error handling. Do NOT generate any code yet.

Pass 2: Targeted refactoring — apply one category at a time:

Refactor ONLY the raw pointer member variables in this class to use smart pointers.
Do not change anything else. Use C++17. Apply the Rule of Zero.

Doing everything at once often causes AI tools to miss subtle interactions between refactors (e.g., a raw pointer used in a C callback that can’t be wrapped in unique_ptr). One category at a time gives you clean, reviewable diffs.

Tool Comparison

Refactoring Task Claude GPT-4 Copilot
Raw pointer → unique_ptr Excellent — Rule of Zero Good Good inline
noexcept move constructors Includes Sometimes misses Sometimes
C++20 ranges Correct syntax Correct Good in context
std::expected (C++23) Yes Yes Good if context present
Thread safety modernization Meyers Singleton, atomic Good Moderate
Constexpr / consteval Correct Good Good
Template → concepts refactor Strong Strong Good
Structured bindings Proactive Proactive Inline
Custom concept generation Strong Good Needs context
Audit before refactor Excellent Good N/A (inline only)

FAQs

Q: Can AI tools safely refactor code that mixes C and C++?

Yes, with caveats. Raw pointers used with C APIs (like passing a char* to a C library) cannot be wrapped in unique_ptr without adapters. Claude correctly identifies these cases and leaves C-compatible interfaces untouched, wrapping only the internal C++ side. Always specify “this pointer is passed to a C API” in your prompt.

Q: Should I refactor everything to C++23 at once?

No. Refactor incrementally by standard: first target C++11 wins (smart pointers, auto, range-based for), then C++14/17 (structured bindings, std::optional, if constexpr), then C++20/23 features. Each incremental upgrade is smaller, safer to review, and easier to compile-test.

Q: How do I prevent AI from over-modernizing?

Specify your minimum supported compiler and standard in the prompt: “This code must compile with GCC 9 (C++17 support only). Do not use C++20 features.” Claude respects this constraint reliably. GPT-4 occasionally slips in C++20 features; check the output if you’re on an older toolchain.


Built by theluckystrike — More at zovo.one