C++ wird in der Embedded‑Welt häufig kritisch betrachtet. Typische Argumente sind:
- „C++ erzeugt zu viel Overhead“
- „Die STL ist zu groß“
- „Exceptions machen das System unkontrollierbar“
- „Objektorientierung kostet Performance“
Diese Aussagen sind nicht grundsätzlich falsch – sie beziehen sich jedoch meist auf eine PC‑ oder Server‑typische Nutzung von C++.
In Microcontroller‑Projekten kann C++ jedoch auch sehr kontrolliert und effizient eingesetzt werden, ohne messbaren Overhead gegenüber C zu erzeugen. Entscheidend ist dabei, welche Sprachfeatures genutzt werden und welche bewusst vermieden werden.
Dieser Artikel zeigt typische Regeln für „Embedded‑C++“ und erläutert, warum sie wichtig sind.
Verbindung zum objektähnlichen C‑Design
Im Artikel über objektähnliche Programmierung in C wurde gezeigt, wie sich Konzepte wie:
- Instanzen
- Kapselung
- klare Modulgrenzen
bereits mit reinem C umsetzen lassen.
C++ bietet für genau diese Konzepte native Sprachunterstützung:
- Klassen statt struct + Funktionen
- private / public statt opaque struct Pattern
- Konstruktoren für definierte Initialisierung
Damit kann C++ häufig denselben Architekturansatz deutlich einfacher und lesbarer ausdrücken, ohne zusätzliche Laufzeitkosten zu erzeugen.
Beispiel:
C‑Variante:
typedef struct
{
int state;
} controller_t;
void controller_update(controller_t *ctrl);
C++‑Variante:
class Controller
{
public:
void update();
private:
int state;
};
Der erzeugte Code ist bei einfachen Klassen praktisch identisch zu C.
Wichtige Regeln für Embedded‑C++
Damit C++ auf Microcontrollern keinen unnötigen Overhead erzeugt, haben sich einige Designregeln etabliert.
Keine Exceptions
C++‑Exceptions benötigen:
- zusätzlichen Code für Stack‑Unwinding
- Tabellen für Exception‑Handling
- nicht deterministische Laufzeiten
In vielen Embedded‑Projekten werden Exceptions daher komplett deaktiviert.
Typische Compiler‑Option:
-fno-exceptions
Fehlerbehandlung erfolgt stattdessen klassisch über:
- Rückgabewerte
- Statusflags
- explizite Fehlercodes
Kein dynamischer Speicher
Viele Embedded‑Systeme vermeiden Heap‑Allokationen vollständig. In manchen ist dynamische Allokation nur in der Initialisierungsphase sinnvoll. Allgemein sollte sie ohne MMU sehr vorsichtig und mit Bedacht eingesetzt werden.
Gründe:
- Fragmentierung
- schwer analysierbare Laufzeit
- Langzeitstabilität
Daher gilt oft die Regel:
- kein
new - kein
delete - keine Container mit dynamischer Allokation
Stattdessen werden Objekte statisch oder in festen Pools angelegt.
STL nur sehr eingeschränkt verwenden
Die Standard Template Library ist leistungsfähig, bringt aber mehrere Probleme mit sich:
- häufige Heap‑Allokationen
- teilweise sehr große Code‑Generierung
- komplexe Laufzeitverhalten
In vielen Embedded‑Projekten wird die STL daher entweder gar nicht oder nur sehr selektiv eingesetzt.
Typische Alternativen sind:
- einfache statische Container
- eigene Ringbuffer
- fixed‑capacity Arrays
Callbacks nicht über std::function
std::function ist flexibel, erzeugt jedoch häufig:
- dynamische Speicherallokationen
- indirekte Aufrufe
- zusätzlichen Code
In Embedded‑Systemen werden Callbacks daher häufig über Delegate‑Klassen implementiert.
Ein bekanntes Beispiel ist das Konzept der
„fastest possible delegates“. Hierzu gibt es unterschiedliche Implementierungen und Artikel im Internet.
Diese Implementierungen:
- benötigen keinen Heap
- erzeugen keinen unnötigen Overhead
- können direkt auf Memberfunktionen zeigen
Damit erhält man typsichere Callbacks ohne die Kosten von std::function.
Virtuelle Funktionen sparsam einsetzen
Virtuelle Funktionen benötigen eine vtable.
Das führt zu:
- zusätzlichem Speicher pro Objekt
- indirekten Funktionsaufrufen
Der Overhead ist meist klein, kann in sehr ressourcenarmen Systemen jedoch relevant sein.
Daher gilt häufig:
- virtuelle Funktionen nur dort einsetzen, wo echte Polymorphie benötigt wird
- ansonsten statische Bindung verwenden
Templates gezielt einsetzen
Templates sind eines der leistungsfähigsten Features von C++ und erzeugen keinen Laufzeit‑Overhead.
Sie können sogar helfen:
- virtuelle Funktionen zu vermeiden
- compile‑time Polymorphie zu erzeugen
Beispiel:
- Treiber generisch über Templateparameter
- verschiedene Hardwarevarianten zur Compile‑Zeit auswählen
Der Nachteil ist hauptsächlich größerer Compile‑Time Code.
RAII bewusst einsetzen
RAII (Resource Acquisition Is Initialization) wird in Embedded‑Systemen manchmal vermieden – kann aber sehr nützlich sein.
Beispiele:
- automatische Lock/Unlock‑Mechanismen
- sichere Initialisierung von Hardware
- Scope‑basierte Ressourcenverwaltung
Da RAII rein compile‑time basiert ist, entsteht dabei kein zusätzlicher Laufzeit‑Overhead.
Header‑only Implementierungen für Treiber und Registerzugriffe
Ein häufig genutztes Muster in Embedded‑C++ ist die header‑only Implementierung kleiner Zugriffsfunktionen, insbesondere für:
- Getter / Setter
- Bitmanipulationen
- Registerzugriffe
- kleine Treiberabstraktionen
Dabei werden Funktionen direkt im Header (.hpp) definiert und meist als inline implementiert.
Beispiel:
class Gpio
{
public:
inline void set()
{
*port |= pin_mask;
}
inline void clear()
{
*port &= ~pin_mask;
}
private:
volatile uint32_t* port;
uint32_t pin_mask;
};
Der Vorteil: Der Compiler kann diese Funktionen direkt in den Aufrufer integrieren und damit unnötige Funktionsaufrufe vermeiden.
Vergleich: .hpp vs .cpp
Bei klassischen C++‑Projekten wird Implementierung meist in .cpp‑Dateien ausgelagert. Das verbessert Kompilierzeiten und reduziert Header‑Abhängigkeiten.
Bei sehr kleinen Funktionen – wie sie in Treibern oder Hardwareabstraktionen häufig vorkommen – kann dies jedoch Nachteile haben:
- der Compiler sieht die Implementierung beim Aufruf nicht
- Inlining ist schwieriger oder unmöglich
- zusätzliche Funktionsaufrufe entstehen
Wird die Implementierung dagegen im Header definiert, kann der Compiler:
- Funktionen vollständig inline expandieren
- Konstanten propagieren
- unnötige Variablen eliminieren
Gerade bei Hardwarezugriffen kann dadurch Code entstehen, der identisch zum handgeschriebenen Registerzugriff in C ist.
Beispiel: Registerabstraktion ohne Overhead
class Timer
{
public:
inline void enable()
{
reg->CTRL |= ENABLE_BIT;
}
private:
volatile TimerRegisters* reg;
};
Mit Optimierung erzeugt der Compiler häufig exakt denselben Maschinencode wie bei direktem Zugriff auf das Register.
Der Vorteil liegt daher nicht in zusätzlicher Funktionalität, sondern in:
- besserer Strukturierung
- klaren Schnittstellen
- stärkerer Typprüfung
Templates und Header‑only Bibliotheken
Viele Embedded‑Bibliotheken nutzen Header‑only Designs auch deshalb, weil Templates nur im Header vollständig sichtbar sein müssen.
Dies ermöglicht z.B.:
- Treiberparameter als Templateargumente
- Compile‑Time Auswahl von Hardwarekonfigurationen
- sehr aggressive Optimierungen durch den Compiler
Der Preis dafür sind meist etwas längere Compile‑Zeiten, während der erzeugte Code häufig sogar effizienter wird.
Damit eignet sich der header‑only Ansatz besonders für:
- Hardwareabstraktionsschichten (HAL)
- Treiberbibliotheken
- Registerzugriffs‑Wrapper
In diesen Bereichen lässt sich mit C++ eine sehr saubere Abstraktion erzeugen, ohne zusätzliche Laufzeitkosten gegenüber C.
Vorteile von C++ gegenüber objektähnlichem C
Wenn die oben genannten Regeln eingehalten werden, bietet C++ mehrere praktische Vorteile gegenüber reinem C.
Einfachere Kapselung
In C++ sind Zugriffsebenen direkt Teil der Sprache:
- private
- protected
- public
Dadurch wird die Modulstruktur klarer und weniger fehleranfällig als das opaque‑struct‑Pattern in C.
Klarere Initialisierung
Konstruktoren stellen sicher, dass Objekte immer korrekt initialisiert werden.
Dies reduziert typische Fehlerquellen wie:
- vergessene Init‑Funktionen
- teilweise initialisierte Strukturen
Bessere Typprüfung
C++ bietet strengere Typprüfungen als C.
Dies verhindert viele Fehler bereits zur Compile‑Zeit.
Lesbarer Code
Viele Architekturkonzepte lassen sich direkter ausdrücken:
- Klassen
- Interfaces
- Namespaces
Dadurch wird der Code häufig kürzer und verständlicher, obwohl die erzeugte Maschinecode‑Struktur identisch bleibt.
Wann C++ besonders sinnvoll ist
C++ spielt seine Stärken besonders aus bei:
- größeren Embedded‑Projekten
- komplexeren Softwarearchitekturen
- vielen wiederverwendbaren Komponenten
Gerade wenn ohnehin bereits objektähnliche Strukturen in C implementiert werden, kann C++ diese Architektur oft deutlich eleganter ausdrücken, ohne zusätzliche Laufzeitkosten zu verursachen.
Fazit
C++ ist auf Microcontrollern keineswegs automatisch schwergewichtig oder ineffizient.
Mit einem bewusst eingeschränkten Sprachsubset – häufig als Embedded‑C++ bezeichnet – lassen sich viele Vorteile der Sprache nutzen:
- bessere Kapselung
- klarere Architektur
- stärkere Typprüfung
ohne die typischen Probleme klassischer C++‑Desktopprogramme zu übernehmen.
Entscheidend ist nicht die Sprache selbst, sondern wie sie eingesetzt wird.
