Contact Us
Technical deep-dives November 4  •  15 min read

Legacy C++ modernization: moving C++98 codebases to modern C++ safely at scale

What modern C++ actually means for a multi-million-line codebase: memory-safety roadmaps, idiom rewrites with real gotchas, God classes, ABI breaks, and when to migrate instead.
← All posts

Most large C++ systems in banking, avionics, telecom and industrial control were written before modern C++ existed. They compile with old flags, own memory through raw new and delete, hide logic in macros, and carry classes with tens of thousands of lines. That code works, and it is also exactly what US and allied security agencies now point to when they talk about memory-unsafe software.

This post is for CTOs, architects and senior engineers who own a C++98 or C++03 codebase of a million lines or more and need a plan that survives contact with the real code. It covers why the pressure is real in 2026, which C++ refactoring steps are mechanical and which are not, the before-and-after code for each idiom (with the semantic traps), how to handle God classes, header tangles and ABI breaks, and when a move to Rust, C# or Java makes more sense than modernizing in place. It sits in our complete technical guide to legacy software modernization, which covers the wider strategy.

Key takeaways

  • The CISA and FBI Product Security Bad Practices guidance asks makers of existing C and C++ products to publish a memory safety roadmap by the end of 2025. Modernizing to modern C++ is a credible part of that roadmap, but not the whole of it.
  • Idiom upgrades (NULL to nullptr, auto_ptr to unique_ptr, macros to constexpr) look mechanical, yet several change overload resolution, lifetimes or evaluation order. Treat each as a semantic change with tests.
  • The expensive problems are structural: God classes, header dependency chains, internal APIs used in 10,000 places, and cloned code. Regex and IDE refactorings do not scale to those.
  • Toolchain upgrades come first. You cannot use C++17 or C++20 features until the whole build, including third-party binaries, moves past old ABIs.
  • Rewriting millions of lines in Rust is rarely realistic. A mixed strategy (modern C++ for the bulk, a memory-safe language for new, exposed components) is usually the honest answer.

What is modern C++?

Modern C++ is C++ written to the C++11 standard or later in the style those standards enable: ownership expressed in types (std::unique_ptr, RAII), no manual new/delete, nullptr, enum class, constexpr instead of macros, move semantics, range-based loops, standard threads and views such as std::string_view and std::span. It is a coding discipline, not just a compiler flag.

That distinction matters. A codebase compiled with -std=c++17 can still be C++98 in every line. Conversely, the jump in value comes less from new syntax than from making ownership, lifetime and intent visible to the compiler and to static analysis.

The C++ Core Guidelines, edited by Bjarne Stroustrup and Herb Sutter, are the closest thing to a definition; their stated aim is to help people use modern C++ effectively. Rule I.11 ("Never transfer ownership by a raw pointer (T*) or reference (T&)") summarizes most legacy C++ bugs: if there is doubt about who owns an object, leaks or premature destruction follow.

Why legacy C++ modernization is on the agenda in 2026

Three pressures arrive at once: security policy, toolchain decay and people.

C++ memory safety and the government roadmap guidance

In December 2023, CISA, NSA, FBI and partner agencies in Australia, Canada, the UK and New Zealand published The Case for Memory Safe Roadmaps. It names C and C++ as memory-unsafe languages and cites two numbers most engineers have heard: about 70 percent of Microsoft CVEs (2006–2018) and about 70 percent of vulnerabilities found in Google's Chromium project are memory safety bugs. It asks manufacturers to publish roadmaps with dated phases, a date after which new code is written only in memory safe languages, and a plan for existing C and C++ dependencies.

Then CISA and the FBI published Product Security Bad Practices. Version 1.0 (October 2024) asked for a roadmap by January 1, 2026; Version 2.0 (January 2025) says existing products written in memory-unsafe languages should have a published memory safety roadmap "by the end of 2025." Products with an announced end-of-support date before January 1, 2030 are exempt. The guidance is voluntary and aimed at software that serves critical infrastructure or national critical functions, but procurement teams read it, and so do auditors.

Two points get lost in the headlines. Neither document orders anyone to rewrite C++; the 2023 guide discusses wrapping existing code and rewriting self-contained components. And the roadmap is about prioritization: network-facing and cryptographic code first. A modernized C++ codebase with smart-pointer ownership, bounds-aware views, sanitizer coverage and a defined boundary for memory-safe components is a much easier roadmap to write than a C++98 codebase nobody can analyze. We cover the broader security framing in security debt as a deadline.

Toolchains age out

Old compilers stop receiving fixes, stop running on supported operating systems, and stop compiling current releases of the libraries you depend on. The longer the delay, the more of the upgrade is forced at once.

People

New hires learned C++ from C++11 onward, and code built on auto_ptr, hand-rolled reference counting and macro-generated classes is slower and riskier for them to change. That is the C++ version of the legacy code talent shortage: the language is alive, but the dialect is not.

From C++98 to C++23: what changed and why it matters

The ISO standard has moved every three years since 2011. According to isocpp.org, the current published standard is C++23, formally ISO/IEC 14882:2024, with C++26 having been finalized and is awaiting final administrative approval and formal publication . The table lists the features that matter most when moving a legacy codebase forward.

StandardFeatures that change legacy codeTypical modernization use
C++98/03Baseline: STL, auto_ptr, no move semanticsStarting point for most legacy systems
C++11unique_ptr/shared_ptr, move semantics, nullptr, enum class, auto, range-for, lambdas, std::thread, override, constexprOwnership, threading, type safety; the largest single jump
C++14std::make_unique, generic lambdas, relaxed constexprRemoves most remaining new expressions
C++17std::string_view, std::optional, std::variant, structured bindings, if constexpr, inline variables, std::filesystem; auto_ptr removedReplaces macro constants, sentinel return values, boost:: equivalents
C++20Concepts, ranges, std::span, modules, coroutines, std::jthread, designated initializersBounds-aware views, readable template code
C++23std::expected, std::print, std::to_underlying, standard library modulesError handling without out-parameters or exceptions

The removal of auto_ptr is the forcing function many teams discover late: per cppreference, it was deprecated in C++11 and removed in C++17, so a codebase that uses it cannot move to C++17 without a migration. Most teams target C++17 or C++20 as the first landing point.

Idiom-level C++ refactoring, with before and after code


Idiom-level C refactoring with before and after code

Each idiom below has a tool that rewrites it and at least one case where the rewrite changes behavior. That is why C++ modernization at scale is an engineering program, not a find-and-replace.

Raw new/delete to unique_ptr and RAII

Before, ownership is a convention documented (maybe) in a comment:

// C++98: who owns hdr? Who deletes msg? Read the comments and hope.

Message* parse_message(const char* data, size_t len)

{

Message* msg = new Message;

Header* hdr = new Header;

if (!read_header(data, len, hdr)) {

delete hdr;

delete msg;

return NULL;

}

msg->set_header(hdr); // Message takes ownership (per a comment)

if (!read_body(data + HDR_SIZE, len - HDR_SIZE, msg)) {

delete msg; // relies on ~Message deleting hdr

return NULL;

}

return msg; // caller must delete

}

If read_body throws, msg and hdr leak. If someone later changes ~Message to stop deleting the header, nothing tells them about this function. After:

// C++20: ownership is in the types; every exit path is correct by construction.

std::unique_ptr<Message> parse_message(std::span<const std::byte> data)

{

auto hdr = std::make_unique<Header>();

if (!read_header(data, *hdr)) {

return nullptr;

}

auto msg = std::make_unique<Message>();

msg->set_header(std::move(hdr)); // set_header(std::unique_ptr<Header>)

if (!read_body(data.subspan(Header::wire_size), *msg)) {

return nullptr;

}

return msg;

}

The gotcha is that this change is not local. Message::set_header changes signature, every caller of parse_message changes, and every place that stored the returned pointer in a container or a member must now hold a unique_ptr or borrow a raw observer pointer. In a real codebase, deciding which pointers are owning and which are observing requires data-flow analysis across translation units. Guideline R.11 ("Avoid calling new and delete explicitly") is easy to state and hard to apply without that analysis.

Use shared_ptr only where ownership is genuinely shared. Blanket conversion adds atomic reference counting to hot paths and creates cycles that leak like the old code.

auto_ptr to unique_ptr

// C++98

void process(std::auto_ptr<Session> s);

std::auto_ptr<Session> s(new Session(cfg));

process(s); // ownership silently transferred; s is now null

s->close(); // null dereference at runtime

// C++11 and later

void process(std::unique_ptr<Session> s);

auto s = std::make_unique<Session>(cfg);

process(std::move(s)); // transfer is now visible at the call site

s->close(); // still a bug, but bugprone-use-after-move flags it

A faithful conversion preserves behavior, including the bug, but makes the transfer visible to readers and static analysis.

NULL to nullptr

void log_event(int code);

void log_event(const char* message);

log_event(NULL); // depending on how NULL is defined: calls log_event(int)

// or fails to compile as ambiguous

log_event(nullptr); // calls log_event(const char*)

Replacing NULL with nullptr is the safest bulk change in this list, and it still changes overload selection between integer and pointer parameters. Review those cases specifically. A text replacement also hits NULL inside string literals, comments and identifiers such as NULL_HANDLE.

Macros to constexpr, templates and enums

// C++98

#define MAX_RETRIES 5

#define SQUARE(x) ((x) * (x))

#define MIN(a, b) ((a) < (b) ? (a) : (b))

int s = SQUARE(next_sample()); // next_sample() is called twice

long t = MIN(timeout_ms, limit); // int and long mixed: compiles

// C++17

inline constexpr int max_retries = 5;

template <typename T>

constexpr T square(T x) { return x * x; }

int s = square(next_sample()); // called once

long t = std::min<long>(timeout_ms, limit); // explicit common type required

Two traps. std::min(timeout_ms, limit) with an int and a long does not compile, so a mechanical replacement breaks the build in exactly the places where the macro was hiding a type mismatch. And std::min returns a const&, so binding its result to a reference when the arguments are temporaries creates a dangling reference. On Windows, <windows.h> defines min and max macros that collide with std::min unless NOMINMAX is set. Guideline ES.31 says not to use macros for constants or functions, but the real work is in conditional compilation, which we cover under tooling.

C-style casts to named casts

const Config* cfg = get_config();

Config* c = (Config*)cfg; // silently casts away const

Widget* w = (Widget*)base; // static_cast if Widget is complete here,

// reinterpret_cast if it is only forward-declared

The second line is a classic latent bug: if Widget is only forward-declared in that translation unit, the C-style cast performs a reinterpret_cast, which skips the pointer adjustment that multiple inheritance needs. Converting to static_cast<Widget*>(base) turns that silent bug into a compile error, which is what you want. Guidelines ES.48 and ES.49 ask you to avoid casts, and to use a named cast when you must; the clang-tidy check cppcoreguidelines-pro-type-cstyle-cast finds them.

Platform threads to std::thread and std::jthread

// Win32, C++98

static DWORD WINAPI worker(LPVOID arg)

{

Queue* q = (Queue*)arg;

EnterCriticalSection(&g_cs);

drain(q); // drain() may call EnterCriticalSection again

LeaveCriticalSection(&g_cs);

return 0;

}

HANDLE h = CreateThread(NULL, 0, worker, &queue, 0, NULL);

WaitForSingleObject(h, INFINITE);

CloseHandle(h);

// C++20

std::recursive_mutex g_mutex; // keep recursion semantics for now

std::jthread worker([&queue](std::stop_token st) {

std::scoped_lock lock(g_mutex);

drain(queue, st);

}); // joins automatically on destruction

The trap: a Win32 CRITICAL_SECTION is recursive, and std::mutex is not. Mapping one to the other mechanically produces deadlocks wherever a thread re-enters a lock it already holds. Map to std::recursive_mutex first, then remove recursion deliberately. Thread priority, affinity and named threads are not in the standard library; those calls stay platform-specific via native_handle().

Enums, loops, views and move semantics

enum class. Plain enums leak their names into the enclosing scope and convert silently to int, so if (state == 3) compiles. Switching to enum class Status : std::uint8_t turns every implicit conversion into a compile error, which gives you a precise worklist. Keep the underlying type explicit if values are persisted or sent on the wire.

Range-based for. A range-for evaluates end() once. An index loop that appends to the vector it iterates (for (i = 0; i < v.size(); ++i) { ... v.push_back(...); }) picks up new elements; the range-for version iterates over invalidated iterators, which is undefined behavior. The clang-tidy loop-convert check assigns confidence levels (safe, reasonable, risky) for this reason.

string_view and span. They remove copies and carry bounds, but they do not own data. std::string_view name() const { return first_ + " " + last_; } returns a view of a destroyed temporary. And string_view is not null-terminated, so passing sv.data() to fopen or any C API is a bug.

Move semantics. Adding move constructors to legacy classes speeds up containers, but only if they are noexcept. During reallocation, std::vector copies elements whose move constructor might throw. Classes that violate the rule of three (a destructor that frees, with default copy) must be fixed before any move work, or the moves will double-free.

Modules. C++20 modules and C++23 import std can cut build times but need support across your whole toolchain. Treat them as a late phase, after headers are untangled.

Idiom summary

Legacy idiomModern replacementclang-tidy checkSemantic risk
new/delete, raw owning pointersunique_ptr, make_unique, RAIImodernize-make-unique, cppcoreguidelines-owning-memoryHigh: ownership must be inferred across files
auto_ptrunique_ptr + std::movemodernize-replace-auto-ptrMedium: transfer points, headers you cannot edit
NULL, 0 as pointernullptrmodernize-use-nullptrLow, except overloads
#define constants and functionsconstexpr, templates, enum classmodernize-macro-to-enumMedium: types, double evaluation, #ifdef
C-style castsstatic_cast, const_cast, dynamic_castcppcoreguidelines-pro-type-cstyle-castMedium: hidden reinterpret_cast
Index/iterator loopsrange-for, algorithms, rangesmodernize-loop-convert, modernize-use-rangesMedium: end() evaluated once
typedefusingmodernize-use-usingLow
Missing overrideoverridemodernize-use-overrideLow; often finds real bugs
Win32/pthreadsstd::thread, std::jthread, std::mutexNone (manual)High: recursion, priorities
const char* + lengthstring_view, spanmodernize-use-string-viewMedium: lifetimes, null termination

Structural refactoring: God classes, headers, APIs and clones

Moving to modern C++ idioms makes code safer line by line. Structural work makes a large system changeable again, and it is where most of the cost sits.

God classes

A 40,000-line Document or SessionManager class with 600 methods and 150 members is common in systems that grew for 25 years. Splitting it means grouping members by which methods actually use them, extracting cohesive classes, and then rewriting every call site, including those reached through templates, macros and friend declarations. The analysis is tractable; the call-site rewrite across thousands of files is what breaks manual efforts.

Header dependency hell

When one header change rebuilds thousands of translation units, engineers stop improving shared code. The fixes are known: forward declarations, the pimpl idiom, splitting "everything" headers, removing accidental transitive includes. Applying them everywhere requires knowing which declarations each file uses.

API restructuring

Moving 12,000 call sites from an in-house string class, a home-grown smart pointer or a deprecated framework API to a new one is the most common request we see. The rewrite must respect overloads, implicit conversions and argument evaluation order, and it must be complete, because a half-migrated API is worse than either end state.

Dead code and clones

Large C++ systems carry unreachable functions, unused configurations and copy-pasted blocks that were fixed in one place and not the others. Removing dead code and consolidating clones shrinks what every later step has to touch. The control-flow side of this work is covered in our post on restructuring spaghetti code and GOTOs, and the discovery side in legacy code analysis and business rules extraction.

Toolchain upgrades and ABI breaks

You cannot adopt modern C++ features until every component compiles with a current compiler, and you cannot always mix binaries across compiler generations. Plan the toolchain move as its own phase.

On Windows, Microsoft documents that MSVC build tools from Visual Studio 2013 and earlier do not guarantee binary compatibility across major versions, while Visual Studio 2015 and later are binary-compatible, provided the final link uses build tools at least as new as the newest component. Code built with /GL or /LTCG must use exactly the same build tools throughout. A third-party DLL built with Visual Studio 2008 that exports C++ classes is therefore a migration blocker until you rebuild it, replace it, or wrap it behind an extern "C" interface.

On Linux, GCC 5.1 introduced a new libstdc++ ABI for std::string and std::list, controlled by _GLIBCXX_USE_CXX11_ABI. Linker errors mentioning std::__cxx11 usually mean objects built with different settings are being mixed, often because a vendor library was compiled with an older GCC.

Before touching a line of application code, inventory:

  • Every compiler, version and flag set in use, including embedded cross-compilers.
  • Every prebuilt binary you link against and whether you have its source.
  • Every #ifdef configuration that ships, since each one is a different program.
  • Language extensions in use (MSVC-specific keywords, GCC attributes, old template quirks that newer, stricter compilers reject).

Clang-tidy modernize, sanitizers, and why regex refactoring fails at scale

What clang-tidy gives you

The LLVM project's clang-tidy check list includes more than 50 modernize-* checks, from modernize-use-nullptr and modernize-use-override to modernize-replace-auto-ptr, modernize-make-unique and modernize-use-std-format. Because clang-tidy works on the Clang AST, it understands types and overloads, which already puts it far ahead of text tools. For a codebase that builds cleanly with Clang, running the low-risk checks with fix-its is the cheapest first pass available.

Its limits show up at scale:

  • One configuration at a time. It analyzes code as preprocessed for one set of macros. Code in inactive #ifdef branches is never seen, so platform-specific branches stay unmodernized or get out of sync.
  • Local, per-pattern fixes. Checks rewrite a pattern where they find it. They do not infer ownership across translation units, split classes, or migrate an internal API.
  • Headers and macros. Fixes inside macros are often skipped by default, and fixes in shared headers must be deduplicated across thousands of translation units.
  • Clang must parse your code. MSVC-only code or old compiler dialects may need work before clang-tidy can see it at all.

Sanitizers prove behavior, they do not change it

AddressSanitizer detects out-of-bounds accesses on the heap, stack and globals, use-after-free, use-after-return, double free and leaks, with a typical slowdown of about 2x. Run ASan and UBSan builds of your tests before and after every pass, and record pre-existing bugs separately so they are not blamed on the refactoring.

Why regex and IDE refactorings do not scale

A regex does not know that get() on line 40 is your deprecated API and get() on line 41 is std::unique_ptr::get. It cannot see through typedef, templates, argument-dependent lookup or macros. IDE refactorings understand more, but they are interactive, scoped to one project configuration, and not reproducible: you cannot rerun last month's 4,000 manual clicks on this week's code. And the codebase keeps changing under you during the project.

What AST-based transformation with full name and type resolution adds is the ability to express a change once, as a rule over the program's structure, apply it everywhere it matches, and rerun it on a fresh snapshot until the result compiles and passes tests. We explain the mechanics in how deterministic software transformation works.

ApproachUnderstands types/overloadsSees all #ifdef branchesCross-file changesRepeatable on new snapshot
Regex / sedNoYes, but blindlyYes, blindlyYes
IDE refactoringPartlyNoLimitedNo
clang-tidy fix-itsYesNo (one config per run)Local patterns onlyYes
Custom AST transformation engineYesPossible, if the parser handles conditionalsYesYes

Modernize in place or migrate: C++ to Rust, C# or Java

Memory safe languages are the long-term answer the agencies point to, and for some code they are the right one now. For most multi-million-line systems, they are part of the answer, not the whole of it.

OptionBest fitCosts and risks
Modern C++ in placeLarge, performance-sensitive or certified systems; most of any C++ estateDoes not make C++ memory safe; reduces bug classes and makes analysis tractable
C++ to RustNew components, parsers, network-facing and cryptographic code; small self-contained librariesOwnership redesign, FFI boundary, team retraining; full rewrites of large systems rarely finish
C++ to C#Windows business applications and UI layers, often already mixed with .NETGarbage-collected runtime, performance and determinism trade-offs; C++/CLI bridges during transition
C++ to JavaServer-side business logic headed to JVM platformsSame runtime trade-offs; less common for C++ origins
Wrap and isolateCode you cannot change soonContains risk at the boundary; leaves the internals as they are

A balanced C++ to Rust plan follows the 2023 guidance itself: start with new and smaller components, prioritize security-critical code, and put a memory-safe wrapper around public interfaces where a rewrite is not feasible. Meanwhile, modernize the remaining C++ so ownership is explicit, which makes the eventual boundary cleaner. Teams in regulated sectors face the same trade-off with other languages; see our posts on Ada modernization for safety-critical systems and modernizing Fortran 77 code.

A C++ to C# migration is a translation, not a modernization: deterministic destructors become IDisposable, and memory and threading models change. It fits a Windows application in a .NET shop, rarely real-time or embedded code.

GenAI assistants are useful for explaining unfamiliar modules and drafting tests. For changes that must be applied identically across millions of lines, their output varies run to run and needs line-by-line review; we cover the reasons in why GenAI alone is not enough for modernization.

A step-by-step legacy C++ modernization plan

The sequence follows the same assess-then-transform logic as any large-scale legacy modernization program, adapted to C++.

  • Baseline. Inventory compilers, flags, configurations, third-party binaries and extensions. Measure class sizes, clone rates, dead code and include depth.
  • Build health. Get every shipping configuration compiling on a current compiler at your target standard, warnings on. Fix conformance errors before any style change.
  • Safety net. Add ASan/UBSan builds and characterization tests for the modules you will touch first. Record pre-existing failures.
  • Low-risk modern C++ idiom passes. nullptr, override, using, deprecated headers, auto_ptr. One idiom per commit series, reviewed by exception.
  • Ownership pass. Infer owning versus observing pointers, then introduce unique_ptr, RAII wrappers and span/string_view at boundaries.
  • Structural pass. Untangle headers, split God classes, migrate internal APIs, consolidate clones.
  • Memory safety roadmap. Publish it: prioritized components, the date after which new components are written in a memory safe language, and the C++ code that will be wrapped.
  • Lock it in. Run the same clang-tidy checks in CI so modernized code does not regress.

Pre-flight checklist

  • Every shipping #ifdef configuration builds in CI
  • Prebuilt C++ binaries have source or a C-ABI wrapper plan
  • Sanitizer builds run on the test suite
  • Transformation runs are scripted and repeatable on a fresh snapshot
  • Each pass has an owner, a rollback point and a success metric

How deterministic transformation handles C++ at scale


Why legacy software modernization projects fail

Modernize Software applies modern C++ changes like these with DMS, the Design Maintenance System, a program-transformation engine Semantic Designs has run in production since 1995. Its C++ service page says DMS resolves names and types before changing anything, and handles preprocessor conditionals, templates, overloads, mixed dialects and vendor extensions. Transformations are written as rewrite rules over the parsed program, so a rule that converts an owning pointer pattern or migrates an API is applied identically everywhere it matches.

An illustrative rule (pseudocode, not DMS syntax) for one API migration looks like this:

rule migrate_legacy_string_find:

match expr e.Find(arg) where type(e) == LegacyString

replace e.str().find(arg) and add #include <string>

The practical consequence is repeatability. Rules are developed and tested against a copy while the team keeps shipping; the final pass reruns on a fresh snapshot. Modernize Software reports that the same input produces the same output on every run, and that constructs a rule cannot handle with certainty are flagged for an engineer rather than guessed.

The limits are real. Rules encode decisions, and some decisions are design choices: whether a pointer should be unique_ptr or shared_ptr when the code is ambiguous, or where a God class should split. Heavy template metaprogramming and generated code need specific handling. Tests are still required, because a deterministic rewrite of a buggy program is still a buggy program. The details of what is offered is on the C++ modernization service page.

Case examples: Boeing and Tableau

Both cases are public on Modernize Software's site; the figures below are its claims.

Boeing. Modernize Software reports the automated restructuring of 5+ million lines of C++ avionics mission software into a CORBA-compatible architecture for unmanned aerial vehicles, moving 1990s-era components to a modern component framework. It describes this work as peer-reviewed. The difficulty in a system like this is not syntax: it is changing how components are wired together across millions of lines without altering mission behavior.

Tableau. Modernize Software reports refactoring 4+ million lines of C++ by automatically restructuring APIs and eliminating God classes. Its C++ page lists headline figures of 4M+ lines, 5,000+ modules refactored and under three months start to finish.

Both fit the profile this post describes: structural problems, a codebase too large for manual change, and development that could not stop.

Frequently asked questions

1. Is C++ a memory safe language?

No. US and allied agencies list C and C++ as memory-unsafe because the language permits out-of-bounds access, use-after-free and similar errors. Modern C++ idioms such as unique_ptr, RAII, span and bounds-checked access reduce those bugs significantly, and sanitizers catch many at test time, but they do not provide the guarantees of a memory safe language.

2. Is the US government banning C++?

No. CISA and FBI guidance is voluntary and targets products serving critical infrastructure. It discourages new product lines in memory-unsafe languages and asks makers of existing C and C++ products to publish a memory safety roadmap, by the end of 2025 in the current version. It does not require rewriting existing code.

3. What counts as modern C++?

Modern C++ generally means C++11 or later, written in the style those standards enable: smart pointers instead of manual new and delete, nullptr, enum class, constexpr instead of macros, move semantics, range-based loops and standard threads. Most teams target C++17 or C++20 today; the current ISO standard is C++23, with C++26 being imminent.

4. What does clang-tidy modernize do?

The modernize-* checks in clang-tidy find C++98/03 idioms and suggest or apply fixes, such as modernize-use-nullptr, modernize-use-override, modernize-make-unique and modernize-replace-auto-ptr. They work on the Clang AST, so they understand types, but they analyze one build configuration at a time and make local pattern fixes, not cross-file restructuring.

5. Should we rewrite our C++ codebase in Rust?

Usually not all of it. Rewriting millions of lines is slow and risky, and the agency guidance itself recommends starting with new and smaller components and security-critical code. A common plan is to write new exposed components in Rust, wrap what cannot change soon, and modernize the remaining C++ so ownership is explicit.

6. Can C++ be converted to C# automatically?

Partly. Business logic translates reasonably, but C# has a garbage collector, different destruction timing and no direct equivalent for some templates, pointer arithmetic or multiple inheritance. Automated translation needs rules for those constructs and flagging where a design decision is required. It suits Windows business applications more than real-time or embedded code.

Start with a C++ codebase assessment

If you need a memory safety roadmap that names real components and real dates, start by measuring what you have: compilers, configurations, ownership patterns, class sizes and clones. Modernize Software offers a free codebase assessment with a complexity report, automation estimate and rough timeline. Request an assessment or read more about modernizing large C++ systems without a rewrite.