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.

Related Reading


Built by theluckystrike. More at zovo.one