Modern C++ Firmware: Proven Strategies for Tiny, Critical Systems (Part 1/10)

The Case for Modern C++ on Tiny, Safety Critical Targets

Modern C++ Belongs in Firmware. But if you spend time around embedded teams, you eventually hear the same lines:

  • “C++ is too heavy for microcontrollers.”
  • “The STL will blow our flash budget.”
  • “Exceptions and RTTI are a nonstarter.”
  • “Toolchains for modern C++ are a science experiment.”

There is a tiny grain of truth hiding inside each of these, but if you accept them at face value you lock yourself into 1990s C on 2020s hardware, with all of the usual footguns intact.

This series is about a different path: using modern C++ (C++20 today, selected C++23 features as they mature) on small Cortex M class devices while keeping deterministic timing and tight memory budgets. The companion angle is equally important: using GitLab pipelines and automated checks to enforce the rules so they do not drift over time.

Part 1 sets the stage. The later parts will get into state machines, concepts, memory discipline, host first testing, hardware in the loop, and GitLab CI details. Before we touch any code, we need to be clear about the problem we are actually solving.


Embedded Teams Actually Care About

Most embedded teams I work with are not worried about syntax. They are worried about this list:

  • “Will this code hit its deadlines every single control tick?”
  • “Can I prove that worst case execution time is acceptable?”
  • “Will this firmware still build and run correctly in ten years when we touch it again?”
  • “Does it fit in the 128 KB we have left, and does it stay there after two more releases?”
  • “Can a new engineer understand the behavior without reverse engineering the entire codebase?”

Those are the real constraints. The language is just a tool that can help or get in the way.

Typical context:

  • MCU: Cortex M0/M3/M4, 32 to 64 KB of SRAM, 128 to 512 KB of flash.
  • Timing: 100 to 1000 Hz control loops, with some interrupt driven work on top.
  • Domain: safety pressure from aviation, automotive, industrial, or medical.
  • Lifecycle: 10 years of support, often more.

None of that conflicts with modern C++. It conflicts with sloppy use of any language.

You can absolutely wreck a microcontroller with C: unbounded recursion, dynamic allocation in the control loop, ad hoc linked lists in ISR context, vague integer units, and global state everywhere. You can also write very tight, predictable C++ that compiles to the same or better assembly as an equivalent C implementation, with far fewer ways to lie to yourself.


Where the C++ Reputation Comes From

The reputation comes from legitimate problems that are easy to hit if you treat C++ as a bag of features with no rules.

A short, non exhaustive list:

  • Exceptions that unwind stack frames and emit large tables in your binary.
  • RTTI and virtual inheritance used in hot control paths.
  • std::vector and std::string used as if heap allocation were free and predictable.
  • std::function and fully generic std::bind in inner loops.
  • iostreams, locale aware formatting, and dynamic allocation inside logging code compiled straight into the target.

If that is your reference point, “C++ is too heavy” is a fair conclusion.

The mistake is assuming those are mandatory. They are not. You can:

  • Turn exceptions off.
  • Turn RTTI off.
  • Ban std::function and iostreams from target code.
  • Confine dynamic allocation to startup or to host only tools.
  • Use a very small, well understood subset of the standard library on target.

The interesting part is that you can take advantage of modern language features at the same time.


What Modern C++ Actually Gives You

Here are some features from C++20 (and a few from C++23) that are genuinely useful in firmware:

  • constexpr and constinit
    Move work from runtime to compile time. Build lookup tables, unit conversion functions, and configuration validation so the compiler does the heavy lifting and the microcontroller just reads data.
  • std::array and std::span
    Fixed capacity containers and safe views over raw buffers. They are simple building blocks for deterministic memory usage and safer APIs than raw pointer plus size pairs.
  • Concepts
    Compile time interface contracts. You can express “this type is a hardware platform” or “this type is a communication channel” without vtables, and you get much better diagnostics than template metaprogramming tricks.
  • Designated initializers
    Self documenting initialization for structs and tables. Perfect for large state machine transition tables and configuration blocks.
  • [[nodiscard]] and stronger attributes
    Ask the compiler to complain when a critical return value is ignored, or when a function is misused. That is real leverage in safety critical code.
  • std::chrono
    Type safe time handling. You can stop passing naked integers around that might be milliseconds or ticks or something else that someone remembers but never wrote down.

From C++23, used carefully:

  • std::expected
    Makes error reporting explicit without exceptions. Great for initialization and health checks, especially in “edge of the system” code that runs before the control loop is fully active.
  • Improved constexpr and attributes like [[assume]]
    Help the compiler optimize hot paths and simplify compile time validation without changing distribution behavior.

None of this requires dynamic allocation, exceptions, or runtime polymorphism. The compiler front end gets smarter, the generated machine code can stay as boring and predictable as you want.


Modern C++ Firmware: Zero Cost Abstractions Are Not Marketing

Zero cost abstractions are sometimes treated as marketing copy. In embedded work, you can and should verify the claim.

Take a simple example. Two versions of a buffer API:

// C style
void process_buffer(std::uint8_t* data, std::size_t size) noexcept;

// C++ style
void process_buffer(std::span buffer) noexcept;

If you look at the assembly for a straightforward implementation, you discover two things:

  1. std::span is just a pointer and a size, exactly what you already had.
  2. The compiler inlines it away in most real uses.

You gain:

  • Automatic size pairing with the pointer.
  • Safer overloads that accept std::array, raw arrays, and std::vector from host side code.
  • Clearer intent at the call site.

You do not pay for any of that at runtime if you avoid exotic usage patterns.

The same is true for concepts. A function template constrained with a concept often compiles to the same code as the unconstrained version, but you gain:

  • Compile time checking of the operations that are actually used.
  • Much clearer diagnostics when something does not satisfy the contract.
  • A single place to describe what a “platform” or “communication interface” is supposed to provide.

Modern C++ Firmware: Determinism Comes From Rules, Not From Language Choice

The uncomfortable truth is that most determinism problems are process problems, not language problems.

If you allow:

  • Dynamic allocation in hot paths.
  • Blocking I/O in control loops.
  • Unbounded logging.
  • Unstructured expansion of features with no memory budgets.
  • Manual, ad hoc testing on a single developer board.

then C, C++, Rust, or anything else will hurt you in roughly the same way, just with different error messages.

Determinism comes from:

  • Clear rules about what is allowed on target in different layers of the system.
  • A small set of patterns for state machines, error handling, and hardware abstraction.
  • Tooling that enforces those rules every time someone pushes a change.

This is where GitLab pipelines come in. The CI system becomes the bad cop that refuses to ship a build that:

  • Breaks timing constraints.
  • Overflows flash or SRAM budgets.
  • Reduces test coverage below an agreed threshold.
  • Introduces new undefined behavior, sanitizer failures, or static analysis violations.

Modern C++ gives you more hooks for the compiler and tools to help you. GitLab CI ensures that help is consistently applied.


Modern C++ Firmware: Why Bother With All Of This

You could stay in C and avoid the entire conversation. Many projects do. The cost shows up later:

  • Extra time spent debugging buffer overruns and integer mistakes that the type system could have prevented.
  • Higher cognitive load for new engineers reading maximally terse C code with minimal structure.
  • Difficulty evolving the codebase as requirements change because there are no explicit contracts between layers.

Modern C++ is not free. You pay with:

  • Toolchain setup and validation.
  • Stricter coding guidelines.
  • A learning curve for features that are new to your team.

In return, you get:

  • Better leverage from the compiler.
  • Safer interfaces by default.
  • More testable designs that can run on host and target.

My opinion: if you are building firmware that actually matters, the trade is worth it.


Modern C++ Firmware: What Comes Next in the Series

This first part is intentionally high level. The rest of the series will get more concrete:

  • Part 2 will talk about constraints and standard choice in more detail, and how to write your rules down.
  • Part 3 will cover deterministic design rules: no exceptions, no allocation in the loop, and how to encode that in code and tools.
  • Later parts will cover host first testing, hardware in the loop with Python, observability on the PC, and a concrete GitLab pipeline blueprint.

If you are running C on tiny microcontrollers today and you are tired of fighting the same classes of bugs, the goal is simple: give you a clear, incremental path into modern C++ and CI that improves determinism instead of eroding it.

(Also, check out the Embedded Template Library (ETL) https://www.etlcpp.com/.)


The Complete “Modern C++ Firmware” Series:


Need professional firmware development help? Engage with Polyrhythm!


Discover more from John Farrier

Subscribe to get the latest posts sent to your email.

One thought on “Modern C++ Firmware: Proven Strategies for Tiny, Critical Systems (Part 1/10)

Leave a Reply

Discover more from John Farrier

Subscribe now to keep reading and get access to the full archive.

Continue reading