Direct answer. Don't attempt a wholesale replacement; introduce modern ownership incrementally at module boundaries, converting the highest-risk (most leak-and-crash-prone) areas first behind tests that pin current behavior, while leaving stable, rarely-touched raw-pointer code alone until it needs to change anyway.
Why a full rewrite is the wrong first move
A legacy C++ service with 'sporadic memory bugs' by definition has memory-safety issues you don't fully understand yet; a wholesale rewrite risks introducing NEW bugs in code you didn't need to touch, while the actual crash-causing code might be a small fraction of the codebase. Prioritize by where the actual bugs are, not by 'convert everything to modern C++.'
A phased plan
- Instrument first: add crash reporting/AddressSanitizer (or Valgrind) runs in CI and on a canary population to identify WHERE the sporadic bugs actually originate, rather than guessing.
- Establish characterization tests around the highest-risk modules before changing them, so a refactor that accidentally changes behavior is caught immediately.
- Convert ownership at module boundaries first: wrap raw pointers crossing an API boundary in
std::unique_ptr/std::shared_ptr as appropriate, which fixes the highest-leverage bugs (use-after-free across module boundaries) without requiring every internal raw pointer to be touched simultaneously.
- Introduce a style/lint rule going forward (banning bare
new/delete in new code, requiring smart pointers) so the codebase stops GROWING the problem while the existing backlog is worked down.
- Convert remaining raw-pointer code opportunistically: whenever a module needs a change anyway (a bug fix, a feature), take the opportunity to modernize its ownership as part of that change, rather than scheduling a separate 'convert everything' project that competes with feature work indefinitely.
Reducing risk while modernizing
- Prefer
std::unique_ptr by default (single, clear ownership) and reserve std::shared_ptr for cases with genuinely shared ownership -- reaching for shared_ptr everywhere just because it's 'safer' than raw pointers often just relocates the bugs into reference-cycle leaks instead.
- Where full RAII conversion of a subsystem is too risky to do at once, a smart-pointer WRAPPER around the existing raw-pointer API can buy safety at the boundary while internals are migrated later.
- Track 'sporadic' bugs with sanitizer tooling in a staging environment under load, since many use-after-free/double-free bugs only manifest under specific timing or allocator conditions that a quick manual test won't reproduce.
Trade-offs and pitfalls
- Mixing raw pointers and smart pointers during the transition is itself a hazard: a raw pointer obtained from a
unique_ptr::get() that outlives the unique_ptr is a new use-after-free waiting to happen -- be explicit about which code owns what during the coexistence period.
- Don't declare victory once compilation is clean; a memory-safety migration needs sanitizer-backed testing under realistic load to actually confirm the sporadic bugs are gone, not just that the code still builds.