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