All articles
Regulated software · 4 min read ·

Modernizing legacy C++ without losing compliance

Refactoring old code in a regulated product is not blocked by the standard — it is blocked by traceability. The strategy that works: small, isolated changes with a clear equivalence argument.

C++legacyrefactoringmedical software

This article is the perspective of an engineer who has worked under these requirements, not regulatory advice. For compliance decisions, consult your quality/regulatory specialist.

A lot of the medical software running today is C++ written ten or twenty years ago. It works, it is validated, and nobody wants to touch it. Until they have to: a new requirement appears, a compiler goes out of support, or a library turns up with a vulnerability.

I spent years in this space — real-time imaging components at GE Healthcare and laboratory data system code at Waters. What follows is what I learned about doing it without destabilising everything.

Why is modernization different under regulation?

In ordinary software, a good refactor is one that does not change observable behaviour. Under regulation the bar is harder: you must be able to demonstrate that it did not change.

The difference is enormous. “I am fairly confident it behaves the same” is not an argument. You need evidence — the tests before, the tests after, and a clear link between what you changed and what you verified.

This makes large rewrites nearly impossible to justify. Not because they are forbidden, but because the effort of demonstrating equivalence for an entire system is disproportionate.

How do you start when there are no tests?

The usual case: no tests, no design documentation, original authors long gone.

The order that works:

  1. Characterization tests before anything else. Not “correct” tests — tests that capture current behaviour, quirks included. Your baseline is today’s system, not the ideal specification.
  2. Find the seams. The points where you can introduce a test without rewriting half the module: an interface, an entry point, a free function.
  3. Freeze behaviour at the boundary. If you can test a module’s inputs and outputs, you have complete freedom inside it.
  4. Only then, change things.

Step 1 is the one teams skip most often under time pressure. It is also the one that costs the most when missing.

What does “equivalence” mean, and why does it matter?

When you modify validated code, the question you must answer is: does the result stay the same where it matters?

For numeric code — signal processing, calculations over measurements — this is subtler than it sounds. Reordering floating-point operations can change the last bit of a result. Sometimes that is irrelevant. Other times, if the result crosses a decision threshold, it matters a great deal.

What helps:

  • explicitly define the acceptable tolerance; do not assume “bit-identical”;
  • run comparisons over large sets of real (or representative) data, not three cases;
  • document the differences you find and why they are acceptable.

The equivalence argument is the artifact that convinces. Without it, you only have a promise.

How do you split the work to avoid triggering massive re-validation?

The principle that helped me most: never mix refactoring with behaviour change.

  • one commit that moves code, renames, extracts functions — with no behaviour change whatsoever;
  • a separate, small commit that intentionally changes behaviour.

When the two are mixed, nobody can say what caused a difference, and impact analysis becomes guesswork.

In addition:

  • isolate higher-risk areas so their safety class does not contaminate the rest of the system;
  • group changes by module so retesting stays bounded;
  • treat a toolchain change as a real change — a new compiler is a modification that can affect behaviour, not an infrastructure detail.

Which C++ techniques help most?

From experience, in order of benefit-to-risk ratio:

  • RAII, introduced gradually. Replacing manual resource management eliminates an entire class of defects, and the change is locally verifiable.
  • const-correctness. Cheap, catches errors at compile time, changes no behaviour.
  • Eliminating undefined behaviour. UB is most dangerous in old code: it works until you change the compiler. Sanitizers find a lot, fast.
  • Narrowing visibility. Moving what you can into private translation units reduces the blast radius of future changes.
  • Explicit types instead of implicit conversions, especially at numeric boundaries.

What I learned to avoid: enthusiastically adopting every new language feature. Each change must be justified by benefit, not by modernity.

What not to do

  1. Rewriting from scratch. Almost never justifiable: you lose years of subtle fixes documented nowhere.
  2. “Tidy-up” refactoring in areas you are not touching functionally. Every touch carries verification cost. Free cleanup does not exist here.
  3. Upgrading the compiler and the code at the same time. When a difference appears, you will not know which caused it.
  4. Ignoring third-party dependencies. Every library is a component with its own records; updating is not “just a version bump”.

Modernization under regulation is slower, but it is not impossible. It is essentially the same discipline we should apply everywhere — except here it is not optional.

If you have an old system you need to carry forward without destabilising it, that is exactly the kind of problem I have worked on.

Where I worked under these requirements

  • GE Healthcare — Voluson Ultrasound

    Software Engineer (contract, remote)

    Dec 2022 – Jan 2025

  • Capgemini — client Waters Corporation

    Software Engineer (contract)

    Jun 2022 – May 2023

The project behind this article MedVital — Real-time Vital-signs Monitoring A C++20 vital-signs monitoring engine with a processing pipeline and live streaming — a personal engineering project.

Working on something similar?

If you are building in this space, let us talk for 30 minutes.

Book a call
All articles