C++ auf Microcontrollern – geht auch effizient


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.