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