AI Tools Compared

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;
    }
}

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

Built by theluckystrike — More at zovo.one