From C spaghetti to zero-cost abstractions on MAVLink + RP2040
A practical look at what C++26 brings to firmware development,
and what you can ship today with GCC 15 and -std=c++20
C++26 and GCC 16 are just a few days (maybe weeks) away. Let's play the game: "what if C++26 and GCC 16 are already here?" -- because it takes embedded C++ much further.
Contracts, inplace_vector, and reflection are game changers:
pre/post replace hand-written assert() with standard, toolchain-aware checks. Strip them in release = zero overhead.
Finally a standard resizable container with no heap. Perfect for MAVLink packet buffers.
Auto-generate serialization, debug logs, enum-to-string, all at compile time, zero runtime cost.
Based on the C++26 draft. When compilers catch up, we'll write it this way:
#include <contracts>
#include <inplace_vector>
#include <meta>
template<typename T>
concept Handler = requires(T t) {
t.init();
t.exec();
};
// C++26: resizable container, ZERO heap allocation
// perfect for MAVLink packet buffers
std::inplace_vector<uint8_t, 280> mavlink_buf;
struct MavlinkHandler {
// C++26 contracts: preconditions checked at build/debug
void parse(uint8_t* data, size_t len)
pre(data != nullptr)
pre(len <= 280)
{
mavlink_buf.assign(data, data + len);
}
void init();
void exec();
};
// C++26 reflection: auto-generate debug logs
// for all handler types, zero boilerplate
template<Handler H>
void log_handler_info() {
constexpr auto name = std::meta::name_of(^H);
Serial.print(name.data());
Serial.println(" initialized");
}
template<Handler H1, Handler H2>
struct App {
H1 mavlink;
H2 sensors;
void run() {
mavlink.init();
sensors.init();
log_handler_info<H1>();
log_handler_info<H2>();
multicore_launch_core1([this] { sensors.exec(); });
while (true) { mavlink.exec(); __wfi(); }
}
};
pre / post replace hand-written assert() with standard, toolchain-aware checks. Strip them in release = zero overhead. The compiler and static analyzers can reason about them.
A standard resizable container with fixed capacity and no heap allocation. Drop-in replacement for hand-rolled ring buffers. Perfect for MAVLink packet buffers on microcontrollers.
Auto-generate serialization, debug logs, enum-to-string -- all at compile time with zero runtime cost. No more macro hacks or code generators.
No globals, no vtables, no heap, no RTTI. Just the way embedded should be.
As of April 2026, no compiler supports all C++26 features for production use.
| Feature | Standard | GCC 15 | GCC 16 (trunk) |
|---|---|---|---|
| Concepts | C++20 | Yes | Yes |
| constexpr placement new | C++26 | Yes | Yes |
| =delete("reason") | C++26 | Yes | Yes |
| Pack indexing | C++26 | Yes | Yes |
| #embed | C++26 | Yes | Yes |
| Contracts (pre/post) | C++26 | No | Yes |
| inplace_vector | C++26 | No | Yes |
| Reflection (meta) | C++26 | No | No |
Since no compiler fully supports C++26 today, the next slide shows what you can ship right now.
#include <array>
#include <cstdint>
#include <cstddef>
// zero-cost interface check, no vtable
template<typename T>
concept Handler = requires(T t) {
t.init();
t.exec();
};
// heap-free resizable buffer for MAVLink packets
template<typename T, size_t N>
struct FixedVec {
std::array<T, N> buf{};
size_t len{};
constexpr void push(T v) { buf[len++] = v; }
constexpr T& operator[](size_t i) { return buf[i]; }
constexpr size_t size() const { return len; }
constexpr T* data() { return buf.data(); }
};
struct MavlinkHandler {
FixedVec<uint8_t, 280> rx_buf;
void init() { Serial.begin(57600); }
void exec() { /* parse mavlink */ }
};
struct SensorsHandler {
void init() { /* setup sensors */ }
void exec() { /* read + send */ }
};
// no globals, no pointers, no RTTI
template<Handler H1, Handler H2>
struct App {
H1 core0;
H2 core1;
void run() {
core0.init();
core1.init();
multicore_launch_core1([this] { core1.exec(); });
while (true) { core0.exec(); __wfi(); }
}
};
// one line in main
App<MavlinkHandler, SensorsHandler> app;
void setup() { app.run(); }
void loop() {}