
Warum C++ noch lange nicht tot ist - Rust hin oder her
Zu den Charakteristiken von Embedded Systemen gehören meistens begrenzte Ressourcen, harte Echtzeitanforderungen und jahrzehntelange Produktlebenszyklen. Traditionell dominiert C diese Domäne, denn die Sprache bietet volle Kontrolle über die Hardware und minimalen Overhead. Doch die Komplexität und Anforderungen an moderne Embedded-Anwendungen steigt: Vernetzung, Over-the-Air-Updates, komplexe Zustandsautomaten und Sicherheitsanforderungen fordern ihren Tribut.
Hier kommt modernes C++ ins Spiel. Mit Standards ab C++11 bis C++23 hat sich die Sprache fundamental weiterentwickelt und bietet heute Abstraktionen, die ohne Laufzeit-Overhead auskommen (zero-cost abstractions). Zudem ermöglichen Features wie constexpr, RAII, Templates und starke Typisierung robusteren und wartbareren Code zu schreiben, ohne die Kontrolle oder Effizienz zu opfern, die in der Entwicklung von Embedded Systemen erforderlich ist.
Wieso (noch) nicht Rust?
Rust gewinnt in der Embedded-Welt an Aufmerksamkeit. Die Sprache verspricht Memory Safety ohne Garbage Collector, erzwingt Thread-Safety zur Compile-Zeit und dies bei ähnlicher Leistung wie C/C++. Dies sind attraktive Eigenschaften für sicherheitskritische Systeme. Dennoch bleibt C++ die pragmatischere Wahl für viele Projekte: Die Toolchain-Unterstützung ist ausgereift, bestehender C Code lässt sich schrittweise modernisieren und der Entwickler-Pool ist deutlich grösser.
In diesem Artikel zeigen wir, wie modernes C++ die Lücke zwischen der Kontrolle von C und den Sicherheitsgarantien neuerer Sprachen wie z.B. Rust (teilweise) schliesst.
Container
Arrays sind in Embedded Systemen allgegenwärtig: Buffers, Lookup-Tables, Register-Mappings. C-Arrays sind unsicher, da diese keinen Boundary-Check haben und die Grösseninformation schnell verloren geht. C++11 führt std::array ein und C++20 ergänzt std::span für sichere Array-Views.
std::array - Typsicheres Array mit fester Größe
std::array ist ein Zero-Overhead-Wrapper um C-Arrays. Gleiche Performance, bessere Sicherheit:

Defensive Programmierung mit at(): In Embedded Systemen sind Exceptions oft deaktiviert (-fno-exceptions). Bei einem Out-of-Bounds-Zugriff mit at() ist das Verhalten implementierungsabhängig. Typischerweise ruft die Standardbibliothek std::terminate() auf, was standardmässig das Programm beendet. Das klingt dramatisch, ist aber besser als stille Datenkorrumption durch Buffer Overflows. Mit einem manuellen Check handhabt man die Fälle in denen ein Out-of-Bounds-Zugriff erwartet wird und mit at() fängt Bugs ab die durchrutschen. Mit einem Custom Terminate-Handler lässt sich das Verhalten definieren - etwa Fehler-Logging, Safe-State oder kontrollierter Reset.
std::span - Typsichere Array-Views
std::span ab C++20, ist ein "View" auf ein zusammenhängendes Array. Es löst das Problem der Array-Übergabe. Speicherort und Grösse werden in einem Typ zusammengefasst, welcher einen sicheren Zugriff z.B. mittels Iteratoren erlaubt:

Sub-Spans - Sichere Slicing
std::span erlaubt sicheres Aufteilen von Arrays:

Const-Correctness mit span

Enum Class
Traditionelle C-Enums haben mehrere Schwächen: Sie verschmutzen den globalen Namespace, konvertieren implizit zu int und bieten keine Typ-Sicherheit. C++11 führt mit enum class stark typisierte Enumerationen ein.
Das Problem mit klassischen Enums

Starke Typisierung
enum class löst diese Probleme durch eigene Namensräume und verhindert implizite Konversionen:

Der Compiler erzwingt eine korrekte Verwendung. Tippfehler und Verwechslungen werden zur Compile-Zeit erkannt.
Kontrolle über die Speichergröße
Bei Embedded Systemen zählt jedes Byte. Enum Classes erlauben die explizite Angabe des zugrundeliegenden Typs:

Explizite Konversion wenn nötig
Manchmal ist der Zugriff auf den zugrundeliegenden Wert notwendig, etwa beim direkten Schreiben in Hardware-Register:

Die explizite Konversion macht klar, dass hier bewusst die Typ-Grenze überschritten wird. C++23 führt std::to_underlying() ein, eine Funktion, die den zugrundeliegenden Typ automatisch erkennt. Statt static_cast(prio) schreibt man std::to_underlying(prio) und erreicht damit typsicherer und lesbareren Code.
Berechnungen zur Compile-Zeit
Modernes C++ bietet typsichere Compile-Zeit-Berechnungen direkt in der Sprache. Der Compiler berechnet das Ergebnis und legt es ohne Laufzeit-Overhead direkt im ROM ab.
Constexpr - Compile-Zeit-Berechnungen
Mit constexpr (seit C++11) lassen sich Funktionen und Variablen markieren, die zur Compile-Zeit ausgewertet werden können.

Consteval - Garantierte Compile-Zeit-Auswertung
C++20 führt consteval ein, eine Verschärfung von constexpr. Funktionen mit consteval müssen zur Compile-Zeit ausgewertet werden, ein Aufruf zur Laufzeit ist ein Compiler-Fehler:

Dies garantiert, dass ressourcenintensive Berechnungen niemals versehentlich zur Laufzeit ausgeführt werden.
RAII
RAII (Resource Acquisition Is Initialization) ist ein Idiom in C++ für automatisches Ressourcenmanagement.
Das Grundprinzip
Die Idee ist einfach: Ressourcen (Speicher, Hardware-Peripherie, Interrupts, Mutexe) werden im Konstruktor eines Objekts angefordert und im Destruktor automatisch wieder freigegeben. Der Compiler garantiert, dass Destruktoren aufgerufen werden, selbst bei frühen Returns. In C sieht eine CriticalSection so aus:

Jeder Fehlerfall erfordert manuelles Aufräumen. Bei komplexeren Funktionen führt das zu dupliziertem Cleanup-Code und potenziellen Ressourcenlecks. Mit RAII wird die Ressource in eine Klasse gekapselt:

Smart Pointers
Smart Pointers aus C++11 bringen automatisches Speichermanagement nach C++. In Embedded Systemen ist ihr Einsatz wegen der dynamischen Speicherallokation umstritten. Doch es gibt legitime Anwendungsfälle und Smart Pointers machen diese sicherer.
std::unique_ptr - Exklusiver Besitz
std::unique_ptr garantiert exklusiven Besitz eines Objekts. Es gibt genau einen Owner und beim Verlassen des Scopes wird der Speicher automatisch freigegeben. Diese Abstraktion ist vollständig ohne Laufzeitkosten und der erzeugte Code entspricht einem manuellen delete.

Besitz übertragen mit move
unique_ptr kann nicht kopiert, aber verschoben werden:

Der Compiler erzwingt klare Besitzverhältnisse.
std::shared_ptr - Geteilter Besitz
shared_ptr erlaubt mehrere Besitzer durch Reference Counting. Ressource wird freigegeben, wenn der letzte Owner zerstört wird.
Im Gegensatz zum unique_ptr hat der shared_ptr Overhead und zusätzlicher Speicher für den Reference Counter und atomare Operationen für Thread-Safety ist alloziert. Es empfiehlt sich diesen Pointer Typ nur zu verwenden, wenn es wirklich nötig ist.
Fazit
Die gezeigten Features sind nur ein Ausschnitt, modernes C++ bietet weit mehr: std::optional und std::expected für explizite Fehlerbehandlung, std::variant für typsichere Unions, Concepts für typsicher Template-Constraints und vieles mehr.
Jedoch bleiben die Altlasten von C++ wie z.B. undefiniertes Verhalten, manuelle Speicherverwaltung oder fehlende Borrow-Checker bestehen. Die Verantwortung liegt beim Entwickler neue und sichere Konzepte zu verwenden. Wer weiterhin rohe Pointer statt Smart Pointers nutzt, C-Arrays statt std::array, manuelle Schleifen statt Range-Algorithmen, der verschenkt die Vorteile. Die Sprache bietet sichere Alternativen, erzwingt diese aber nicht. Coding-Standards, Code-Reviews und kontinuierliche Weiterbildung sind darum essenzielle Bestandteile einer erfolgreichen Embedded Software Entwicklung, um modernes C++ auch tatsächlich einzusetzen und dessen Vorteile zu nutzen.
Rust hingegen wurde von Anfang an, mit dem Ziel sicher zu sein, entwickelt. Viele Probleme, um die sich der Entwickler in C++ selbst kümmern muss, werden in Rust bereits sicher durch die Sprache eliminiert. Aus sicherheitstechnischer Sicht ist Rust eine bessere Wahl gegenüber C++.
Allerdings ist Rust im Vergleich zu C++ noch jung, was bedeutet, dass es weniger erfahrene Entwickler und ein kleineres Ökosystem an Bibliotheken gibt. Daher bleibt auch heute noch C++ eine relevante Sprache.
Nicola Jaggi
BSc BFH in Elektro- und Kommunikationstechnik
Embedded Software Engineer
Über den Autor
Nicola Jaggi ist seit 11 Jahren Embedded Software Engineer bei der CSA Engineering AG mit Fokus auf C++ Firmware auf STM32.
CSA Engineering AG unterstützt Unternehmen bei der Konzeption und Entwicklung moderner Embedded Software.