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:
- Raw pointers → smart pointers (
new/delete→std::unique_ptr/shared_ptr) - C-style arrays →
std::array/std::vector/std::span - Manual loops → range-based for /
std::ranges NULL→nullptr- Manual RAII → scope guards / move semantics
std::string+ C-style string mixing → modern string handlingtypedef→using- C-style casts →
static_cast/reinterpret_cast - Mutex + bool flag →
std::atomic - 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 |
Related Reading
- How to Use AI to Debug Segmentation Faults in C and C++ Programs
- How to Use AI for Zig Development
- How to Use AI for WebAssembly Development
Built by theluckystrike — More at zovo.one