Modernizing Legacy C++
Practical patterns for migrating C++98/03/11 codebases to modern C++. Each change is safe to apply incrementally — no big-bang rewrites.
Migration strategy
- Raise the compiler standard flag (
-std=c++17) and fix all errors and warnings first - Run sanitizers (ASan + UBSan) on the test suite — surface latent bugs before touching the code
- Apply clang-tidy
modernize-*checks with--fixon non-critical modules first - Migrate ownership: replace raw
new/deletewith smart pointers, file by file - Replace preprocessor constants and macros with
constexprand templates - Move to standard vocabulary types:
optional,variant,string_view
→ C++11
| Legacy pattern | Modern replacement | Why |
|---|---|---|
| T* ptr = new T() | auto ptr = std::make_unique<T>() | Eliminates manual delete, exception-safe |
| for (int i=0; i<v.size(); ++i) v[i] | for (auto& x : v) x | Reads intent, no off-by-one possible |
| boost::function / tr1::function | std::function | Standard, no Boost dependency |
| typedef std::vector<int> IntVec; | using IntVec = std::vector<int>; | Cleaner, works with templates |
| 0 or NULL for null pointer | nullptr | Type-safe, no integer ambiguity |
| #define MAX_SIZE 256 | constexpr int MAX_SIZE = 256; | Typed, scoped, debuggable |
| std::pair<int,string> make() { return std::make_pair(1, "a"); } | auto make() { return std::make_pair(1, "a"); } | Auto return type deduction |
| pthread_mutex_t / CRITICAL_SECTION | std::mutex / std::lock_guard | Portable, RAII-based |
→ C++14/17
| Legacy pattern | Modern replacement | Why |
|---|---|---|
| if (map.count(k)) { T v = map[k]; ... } | if (auto it = map.find(k); it != map.end()) ... | Single lookup, no double search |
| std::pair<bool,int> result | auto [ok, value] = result; | Structured bindings — cleaner destructuring |
| boost::optional<T> | std::optional<T> | Standard since C++17 |
| boost::variant<A,B,C> | std::variant<A,B,C> | Standard, std::visit support |
| boost::string_ref / llvm::StringRef | std::string_view | Zero-copy, standard |
| std::endl (flushes every line!) | "\n" | std::endl flushes buffer — 10-100× slower for log-heavy code |
| template<class T> enable_if<is_integral<T>::value,T>::type f(T v) | template<std::integral T> T f(T v) | C++20 Concepts — readable constraints |
→ C++20/23
| Legacy pattern | Modern replacement | Why |
|---|---|---|
| Hand-rolled enable_if SFINAE | Concepts with requires | Human-readable error messages |
| Boost.Filesystem / experimental::filesystem | std::filesystem | Standard since C++17 |
| Custom format_to / sprintf chains | std::format / std::print | Type-safe, extensible, fast |
| boost::optional<T> with exception for error | std::expected<T, E> | Error-as-value, no exceptions needed |
| Manual iterator pairs: begin(v), end(v) | std::ranges:: algorithms | Range-based, composable with views |
| Coroutine library (Boost.Coroutine2) | co_await / co_yield | Language-level coroutines, zero overhead |
Tooling for automated migration
clang-tidy modernize-*
Auto-applies many C++11/14/17 upgrades. Run with --fix.
cpp11-migrate
Older clang-based migrator (predecessor to clang-tidy modernize-*).
include-what-you-use
Clean up transitive #include dependencies before migrating.
ASan + UBSan
Run the test suite under sanitizers before migration — surface latent bugs that are hidden in C++98 code.
Compiler -Wdeprecated
GCC/Clang warn on deprecated C++17/20 features (trigraphs, std::iterator base class, etc.).
ABI compatibility warning
If your library has external users, changing from C++14 to C++17 std::string, or from boost::optional to std::optional in public headers, breaks ABI. Bump your SONAME and communicate the change. For header-only libraries this is not an issue.