oop-trainer.de
Ralf Schneeweiß

C++ im Embedded System

Möglichkeiten und Grenzen des Einsatzes von C++
zur Entwicklung von Embedded Systemen


Inhalt:


Einführung

Die Sprache C++ ist nicht mehr die Neueste und wird schon seit geraumer Zeit auch für die Entwicklung von Embedded Software eingesetzt. Ohne es statistisch hart belegen zu können, denke ich, dass es sich heute (2009) bei der Mehrheit der C++-Projekte um Embedded Software Projekte handelt. Was ist also die Intention diesen Artikel zu schreiben, wenn C++ doch schon längst massiv für Embedded Projekte eingesetzt wird?
Die Intention ergibt sich aus der Praxis: aus den sich wiederholenden Fragen, die mit der Wahl von C++ in solchen Projekten einhergehen und die für jedes Projekt neu geklärt werden müssen. Die Sprache C++, insbesondere die Bibliothek, wurde nicht primär für die Erfordernisse von Embedded Systemen entwickelt. Daraus ergibt sich eine spezifische Anpassungsproblematik. Außerdem machen spezifische Voraussetzungen im Projekt Entscheidungen notwendig, die über die reine Anwendung von Standard C++ und der Standard Bibliothek hinausgehen. Insbesondere bei der Integration alter und neuer Quellcodes stellen sich weitere Notwendigkeiten zur Anpassung. Dieser Artikel möchte die wesentlichen Punkte dieser Anpassungsproblematiken beleuchten und dem Leser eine Entscheidungshilfe geben, wie er unter Ausnutzung der Standardmechanismen C++ im Embedded System einsetzen kann.

OOP

C++ ist eine Objektorientierte Sprache, die als Obermenge von C entwickelt wurde1). Das Fehlen einer spezifischen Laufzeitumgebung, die Übersetzung in nativen Maschinencode und die Möglichkeit beliebige C-Konstrukte zu verwenden machen C++ in vielen Einsatzbereichen zu einem natürlichen Ersatz der älteren Vorläuferin. Hinzu kommt die inzwischen breite Verfügbarkeit des freien C++ Compilers aus der GNU Suite und dessen Derivate. Welchen Vorteil erwartet man von der Sprache C++ gegenüber der Vorgängerin C?
Die Objektorientierung als solche ermöglicht eine effektivere Systematisierung des Codes und damit eine beherrschbarere Aufteilung des gesamten Codes in kleinere Einheiten2). Die bessere Beherrschbarkeit erhöht die Produktivität der Entwickler im Team. Objektorientierte Konstrukte erlauben auch eine klarere Abgrenzung der Softwarebestandteile (Module, Komponenten) und nicht zuletzt eine bessere Kommunizierbarkeit der verwendeten Techniken3). Die höhere Produktivität und die bessere Kommunizierbarkeit sind ein nicht zu unterschätzender Kostenvorteil bei der Softwarentwicklung. Da alle diese positiven Aspekte der Objektorientierten Softwareentwicklung bereits hinreichend praktisch belegt sind soll an dieser Stelle das Objektorientierte Paradigma selbst nicht weiter vertieft werden. Vielmehr soll darauf hingewiesen werden, dass Embedded Software sich heute nicht mehr ausschließlich auf Microcontroller beschränkt. In [TR06] werden drei unterschiedliche Größenklassen von Embedded Systems unterschieden: klein, mittel und groß. Die Größe ergibt sich aus dem Funktionsumfang und dieser wiederum bestimmt die Komplexität. Die Steuerungen technischer Anlagen, Geräte der Unterhaltungselektronik, Syteme im Verkehrswesen, in der Telekommunikation und in anderen Bereichen arbeiten mit eingebetteter Software, die an Komplexität so manche PC-Applikation bei weitem übertrifft. Um diese Komplexitäten zu beherrschen müssen geeignete Methoden zur Anwendung gebracht werden. Dabei ist die Objektorientierung nicht die einzige, aber meiner Meinung nach die wesentliche Methode zur Beherrschung der Anforderungen an die Strukturierung moderner Software.

Codegröße und Geschwindigkeit

Grundsätzlich wurde C++ mit dem Ziel der größtmöglichen Effizienz entwickelt. Im Vergleich zu C sollten neben den für die eine Software benötigten Elementen kein Overhead in der Laufzeit, im Speicherverbrauch des Codes und im Speicherverbrauch der Daten entstehen. Alle Elemente der Laufzeit sind so minimalistisch wie möglich, es werden durch den Compiler keine nicht benötigten Elemente zu einer Ausführungseinheit hinzugefügt. Da aber C++ der Objektorientierten Methode folgt, ist es interessant, die einzelnen Elemente diesses Paradigmas in Bezug auf Laufzeit- und Footprint­eigenschaften zu untersuchen. Welche speziellen OO-Konstrukte sind es, die C++ gegenüber C anbietet und welche Kosten verursachen sie?
Es sind vor allem die Klassenbildung, die Ableitung, die Verkettung von Konstruktor- und Destruktoraufrufen sowie die virtuellen Funktionen (auf Exception Handling soll in einem der folgenden Abschnitte eingegangen werden). Extra Laufzeitkosten und Codegröße fallen bei der Klassenbildung nicht an, denn die Klasse in C++ entspricht der Struktur in C. Die Zuordnung der Methoden zu den Klassen erfolgt mit einem einfachen Verfahren der Umordnung der Parameter und führt zu Funktionsaufrufen, wie sie üblicherweise in C realisiert werden. In C++ wird das Objekt der Methode in Form eines Parameters übergeben - der this-Pointer. In C übergibt man Zeiger auf Strukturen, um auf diese zuzugreifen. Die Klassenbildung als solche Verursacht also keinen zusätzlichen Code gegeüber entspechendem C-Code und kann für die Laufzeit wie für den Footprint (Codegröße) als neutral angesehen werden. Ebenso verhält es sich mit der Ableitung, die nur neue Klassen (Strukturen) aufgrund schon vorhandener definiert. Auch die Konstruktoraufrufe sowie deren Verkettung kann als neutral angesehen werden, denn sie ersetzen wiederholte lokale Initialisierungen oder eine zentrale Initialisierung durch eine Initialisierungsfunktion in C. Dabei können Konstruktoren mit geringen Initialisierungsaufgaben inline definiert und durch den Compiler herausoptimiert werden, so dass auch keine Funktionssprünge mehr im Code übrig bleiben. Ähnlich verhält es sich mit den Destruktoren sofern sie nicht virtuell sind. Bei den virtuellen Funktionen muss man etwas genauer hinsehen: es wird bei einem virtuellen Funktionsaufruf eine Entscheidung zur Laufzeit ausgeführt, die zum Aufruf der richtigen Methode führt. Diese Entscheidung wird über die V-Table des Objekttyps getroffen. Die V-Table ist eine anonymes statisches Element jeder Klasse, die Zeiger auf die virtuellen Methoden enthält. Sie existiert also einmal pro Klasse und kann alle Objekte dieser Klasse bedienen. Die Entscheidung müsste in einem entsprechenden C-Code auch gefällt werden und es müsste vom Entwickler dafür Code geschrieben werden. Man kann also sagen, dass ein if-else-Konstrukt oder ein switch-Block die C-Entsprechung zu einem virtuellen Funktionsaufruf in C++ ist. Bezieht man nun mit in Betracht, dass der virtuelle Funktionsaufruf durch den Compiler in einer hochoptimierten standardisierten Weise umgesetzt werden kann, während in C individueller Code optimiert werden muss, dürfte das Pendel in den meisten Fällen zugunsten C++ ausschlagen. Insgesamt kann also auch die Technik der virtuellen Funktionen als neutral für die Laufzeit und die Codegröße betrachtet werden.
Das häufig vorgebrachte Argument, dass die Anwendung objektorientierter Techniken zu einer Verlangsamung des Codes und zu einer Vergrößerung des Footprints führen würde ist im Vergleich von C und C++ falsch. Der aus C++-Quellen erzeugte Maschinencode entspricht grundsätzlich dem aus C-Quellen erzeugten Maschinencode. Die strengere Typisierung in C++ gegenüber C, das Funktionsinlining, die Funktions- und Operatorüberladung sowie die Templateprogrammierung verhelfen dem Compiler eher noch zu einem Vorsprung bei der Optimierung, denn er kann Informationen nutzen, die in einem C-Quelltext gar nicht vorhanden sind.

Die C++ Standardbibliothek

Die eigentliche Herausforderung der Verwendung von C++ im Embedded System dürfte weniger die Syntax als die Verwendung der Standardbibliothek sein. Je nach Plattform, Sicherheitsanforderung oder verwendetem Entwicklungsstandard dürfen bestimmte Teile verwendet werden oder eben nicht. Ein zentraler Punkt ist sicherlich die dynamische Speicherverwaltung. Dabei gibt es neben den C Funktionen malloc() und free() auch die Operatoren new und delete. Diese werden in einigen Bereichen der Bibliothek - wie zum Beispiel in den Containern der STL - auch verwendet ohne direkt in Erscheinung zu treten. Für diese Bereiche muss man ein Bewusstsein entwickeln.
Darf man keine dynamische Speicherverwaltung verwenden, muss man die entsprechenden Bibliotheksbestandteile eben auch beiseite lassen. Man muss die Bibliothek also besser kennen als in der Entwicklung einfacher PC Software.
Neben der Speicherverwaltung gibt es auch andere funktionale Bestandteile der Bibliothek, die Plattformabhängigkeiten aufweisen. So sind alle Elemente, die Zeit abfragen und solche die mit Streaming und mit Signalisierung zu tun haben an die Fähigkeiten der Plattform gebunden.
Der MISRA Standard schließt bestimmte mathematische Funktionen aus, da die Übergabe fehlerhafter Parameter zu undefiniertem Verhalten führen kann.
Die Standardbibliothek muss also gut gekannt werden, wenn sie in einem embedded Prokjekt eingesetzt werden soll.

Anbindung von Hardware

Eine der klassischen Aufgaben die mit der Sprache C gelöst werden ist die Ansteuerung von Hardware. Einer der klassischen Vorteile von C gegenüber anderen Programmiersprachen ist die Möglichkeit jede Adressierung die hardwaretechnisch möglich ist in dieser Sprache optimal formulieren zu können. Programmiersprachen, die einem strengeren syntaktischen Konzept folgen erlauben einen solch flexiblen Hardwarezugriff nicht.
Die Syntax von C++ ist eine Obermenge von C und erbt daher die Möglichkeiten ihrer Vorgängerin vollständig. Die Optimierbarkeit von C++ Code und die erzeugte Codegröße sprechen auch für den Einsatz von C++ als objektorientierten Ersatz von C. Für den Aspekt der Hardwareanbindung spricht also grundsätzlich nichts gegen die Verwendung von C++.

Systemnahe Programmierung

Die HW-Anbindung wird häufig durch Treiber realisiert wenn man ein Betriebssystem einsetzt das eine solche Schnittstelle vorsieht. Treiber sind normalerweise sehr kleine und überschaubare Softwareeinheiten, die die Hardwarestruktur abbilden. Es gibt aber in der Embedded Softwareentwicklung häufig Hardwareanbindungen, die ohne die Treiberschnittstelle eines Betriebssystems - und damit ohne Vorsystematisierung - auskommen müssen.
Insbesondere bei der Verwendung eines Betriebssystems kann es bezüglich der zu benutzenden Bibliotheksfunktionen in C und C++ zu ganz spezifischen Einschränkungen kommen. So ist es beispielsweise im Linux-Kernelspace nicht erlaubt die Speicherallokationen des User-Spaces zu verwenden. Für C ist die Lösung relativ einfach: man hat für solche Operationen einen speziellen Satz von Funktionen, die das Betriebssystem für den Kernelspace vorsieht. In C++ ist die Sache leider nicht ganz so einfach. Speicherallokationen werden dort üblicherweise mit dem Operator new durchgeführt. Die entsprechende Deallokation erledigt delete. Diese beiden Operatoren basieren natürlich auf entsprechenden Runtimefunktionen - in vielen Fällen sind das die User-Space Funktionen malloc() und free() aus der C-Bibliothek. Man kann also die beiden Operatoren new und delete nicht einfach verwenden wenn man C++ Code in einem Kontext anwendet, in dem Speicherallokationen mit speziellen Funktionen durchgeführt werden müssen.
Zur Lösung dieses Problems macht C++ die Überladung der Operatoren new und delete möglich, wie sie in einem separaten Abschnitt beschrieben ist. Ob sich der Aufwand allerdings lohnt, sei dahingestellt und wird sich an der zu lösenden Aufgaben entscheiden.

Der new-Operator

Einige der tiefgriefendsten Probleme beim Entwickeln von portablem und von systemnahem Code mit C++ ergeben sich aus der Definition des new-Operators. Dieser Operator wirft in seiner Standardausführung eine Exception, wenn die Allokation von Speicher fehlschlägt. Dieses Verhalten bekam dieser Operator erst mit der ANSI/ISO-Standardisierung von 1998. Vorher gab er im Fall der fehlgeschlagenen Allokation einen Nullzeiger an die Aufrufende Stelle zurück. Der Operator wurde also mit der Standardisierung umdefiniert, woraus sich einige ganz spezifische Probleme ergeben:

  1. Inkompatibilität zwischen altem AT&T konformem und ISO-Standardkonformem Code.
  2. Anstelle einer Nullpointerüberprüfung müssen Exceptions gefangen werden, wenn der Standard-new-Operator verwendet wird.
  3. Wenn dieser Operator verwendet wird, muss der Code Exception sicher geschrieben werden.
  4. Teile der Standardbibliothek nutzen diese Form der Ausnahmebehandlung. Damit trifft Punkt 1 auch auf Teile der Standardbibliothek zu. Die Verwendung der entsprechenen Teile zwingt zu Exception sicherem Code wie schon in Punkt 3 beschrieben.
  5. Die Entwicklung von Exception sicherem Code stellt hohe Anforderungen an den Kenntnisstand der Entwickler über spezielle Aspekte von C++.
  6. Der Compiler hat in C++ nicht die Möglichkeit, die vollständige Behandlung der Exceptions zu überprüfen, wie es in Java der Fall ist. Damit ist keine hinreichende Unterstützung durch den Compiler für die Verwendung von Exceptions und für den Standard-new-Operator gegeben.

Die Überladung der Operatoren new und delete

Es existieren grundsätzlich zwei Varianten für die Überladung von new und delete:

  1. Die Überladung von new und delete für eine spezifischer Klasse.
  2. Die Überladung der globalen Operatoren new und delete.
class X
{
public:
  static void* operator new( std::size_t s );
  static void operator delete( void*, std::size_t );
};

void* X::operator new( std::size_t s )
{
  if( s != sizeof(X) ) // Wichtig, wenn der Operator
  {                    // nur fuer X wirksam sein soll!
    return ::operator new( s );
  }
  return specialMalloc( s );
}

void X::operator delete( void* p, std::size_t s )
{
  if( s != sizeof(X) ) // dito
    ::operator delete( p );
  else
    specialFree( p );
}
Listing 1:Schema zur Überladung von spezifischen
new- und delete-Operatoren für die Klasse X.

Hier soll zunächst einmal mit dem ersten Punkt begonnen werden: Für alle Klassen können separate new- und delete-Operatoren überladen werden, wenn es der Einsatz-Kontext notwendig macht. Man deklariert die beiden Operatoren als statische Klassenelemente in der Klasse, die an einen system­spezifischen Kontext angepasst werden soll. Die Implementierung des new-Operators fordert einfach die Bereitstellung von Speicher. In der delete-Implementierung muss dieser Speicher wieder zurückgegeben werden. Die beiden Pseudofunktionen specialMalloc() und specialFree() stehen für beliebige spezielle Allokations- und Deallokationsfunktionen. Das besondere an den Implementierungen ist, dass man über einen Parameter die Größe des Speicherbereichs überprüfen kann. Damit kann sichergestellt werden, dass die überladenen Operatoren nur für den Klassentyp zur Anwendung kommen, für den sie definiert wurden. Kindklassen anderer Größe werden damit ausgeschlossen.
Ein Kontext, der ziemlich alle Aspekte der systemnahen Programmierung enthält ist beispielsweise die Treiberprogrammierung unter Linux. Linux Treiber werden im Kernelspace ausgeführt. Dort sind die normalen Speicherallokations­funktionen der C-Lib nicht anwendbar. Es müssen spezielle Funktionen dafür angewendet werden. Mit Listing 2 wird eine mögliche Anpassung an die Erfordernisse des Linux-Kernespaces vorgestellt. Für eine realistische Anwendung des gezeigten Codes muss allerdings noch das Buildsystem für Linux Treiber umgestellt werden, damit C++ Quelldateien in die Makefiles aufgenommen werden können. In der gezeigten Implementierung wurde auf die Abfrage der Speichergröße verzichtet. Damit stellt die Klasse X ihren Allokations- und ihren Deallokationsoperator für sich selbst und alle abgeleiteten Klassen bereit.

Wohlgemerkt: das Beipiel in Listing 2 soll nur zeigen, dass es möglich ist C++ als Sprache an einen Kontext anzupassen, in dem spezielle Voraussetzungen für die Nutzung von dynamischem Speicher gegeben sind (auf keinen Fall soll mit diesem Beispiel gesagt werden, dass C++ für die Treiberentwicklung unter Linux der Sprache C vorzuziehen sei).

...

void* X::operator new( std::size_t s )
{
  // Linux Kernel Funktion zur Allokation.
  void* p = kmalloc( s, GFP_KERNEL );
  return p;
}

void X::operator delete( void* p, std::size_t )
{
  // Linux Kernel Funktion zur Speicherfreigabe.
  kfree( p ); 
}
Listing 2:Mögliche new- und delete-Operatoren der
Klasse X für den Linux-Kernelspace.

In dem vorgestellten Code ist noch keine Fehlerbehandlung realisiert. Auch das ist für den unterstützten Systemkontext ein sehr spezifisches Thema. So ist es beispielsweise nicht ratsam, im Kernelspace Exception Handling zu verwenden. Der umgebende C-Code kennt keine Exception Handler und kann somit auf Exceptions nicht reagieren. Ein Rückgriff auf das Verhalten des „alten“ new-Operators vor der ANSI/ISO-Standardisierung ist einfach zu implementieren. Der new-Operator aus den AT&T-Standards gab einfach einen Nullzeiger zurück, wenn eiune Allokation nicht durchgeführt werden konnte. Allerdings darf dann die Klasse nicht mehr in einem Kontext eingesetzt werden, der Exceptions bei fehlgeschlagenen Allokationen erwartet. Es muss also nicht nur die Klasse durch die Implementierung der kontextspezifischen Funktionen vorbereitet werden sondern auch der Code, der die Klassen nutzt. Mit diesen Voraussetzungen ist eine solche Klasse stark auf den entsprechenden Einsatz-Kontext beschränkt. Wenn beispielsweise eine Klasse auf die in Listing 1 beschriebene Weise mit einem new-Operator ausgesttattet wurde, der die Allokationsfunktionen des Linux-Kernelspaces verwendet, dann kann diese Klasse auch nur im Linux-Kernelspace eingesetzt werden. Das ist der Schwachpunkt der vorgestellten Lösung. Andererseits ist die Implementierung relativ einfach.

Eine Erweiterung dieses Prinzip könnte die Anwendung eines Allokatortyps darstellen. Würde man die beiden Operatoren mit Hilfe eines über Templateparameter gelieferten Typs realisieren, der zwei Methoden - alloc() und dealloc() - implementiert, könnte man über diesen Templateparameter die Klasse an den Systemkontext anpassen, in dem sie verwendet werden soll.


void* operator new( std::size_t size )
{
  // Eine 0-Byte Anforderung wird als
  // 1-Byte Anforderung behandelt.
  // Nach dem Standard ist eine Anforderung
  // von 0 Byte gueltig.
  if( size == 0 ) size = 1;
  for(;;)
  {
    Versuch, Speicher zu allokieren;
    if( Versuch erfolgreich )
      return Zeiger auf Speicher;
    // Die Allokation ist fehlgeschlagen.
    // Jetzt muss die Fehlerbehandlungsfunktion
    // ermittelt werden.
    new_handler globalHandler = set_new_handler( 0 );
    set_new_handler( globalHandler );
    if( globalHandler ) (*globalHandler)();
    else throw std::bad_alloc();
  }
}

void operator delete( void* p )
{
  Freigabe von p;
}
Listing 3:Schema zur Überladung der globalen
new- und delete-Operatoren.

Eine einfache Möglichkeit der Anbindung einer spezifischen Speicherverwaltung an einen beliebigen C++ Code ist die Überladung der globalen new- und delete-Operatoren.

Listing 3 zeigt den schematischen Code einer globalen Überladung der Operatoren. Dabei wird auch deutlich, wie die Ausnahmebehandlung in den new-Operator eingebunden ist. Die globale Handler-Funktion kann noch irgend etwas tun, um Speicher verfügbar zu machen. Wenn das nicht mehr gelingt, wird nach dem ISO-Standard die Exception std::bad_alloc geworfen. In einem Kontext, in dem man sich gegen Exception Handling entscheiden muss kann man das Verhalten einfach dadurch umdefinieren, indem man einen Nullzeiger im Fehlerfall zurück gibt. Technisch ist das sehr einfach zu realisieren. Problematisch wird es erst dann, wenn man Code integrieren möchte, der auf dem ISO-Standard-Verhalten aufbaut und daher mit den Exceptions von new rechnet.
Eine einfache Überladung der globalen Operatoren für den Linux Kernelspace wird in Listing 4 gezeigt.


Während man mit der zuerst vorgestellten Möglichkeit für einzelne Klassen den Allokations- und Deallokationsoperator zu überladen eine große Flexibilität gewinnt, zeichnet sich die zweite Möglichkeit der globalen Umdefinition durch Einfachheit aus. Die globale Überladung definiert für alle Allokationen mit new die Anbindung an eine spezielle Speicherverwaltung. Die loakle Überladung eröffnet die Nutzung unterschiedlicher Speicher­verwaltungen für unterschiedliche Objekttypen.
Problematisch bleibt immer die Änderung des Fehlerverhaltens wenn auf Exceptions verzichtet werden soll. Dann muss garantiert werden, dass kein Code in die Ausführungseinheit integriert wird der die Exceptions nutzt. Andersherum muss bei altem AT&T konformem Code darauf geachtet werden, dass der new-Operator im Fehlerfall einen Nullzeiger liefert. Genauso kann auch die nothrow-Variante des new-Operators überschrieben werden. Die Klassenspezifische Überladung der Allokations- und Deallokations­operatoren erlaubt eventuell auch die Integration beider Codearten indem Operatoren mit unterschiedlichen Fehlerbehandlungs­techniken definiert werden.

void* operator new( std::size_t s )
{
  // Linux Kernel Funktion zur Allokation.
  return kmalloc( s, GFP_KERNEL );
}

void operator delete( void* p )
{
  // Linux Kernel Funktion zur Deallokation.
  kfree( p );
}
Listing 4:Mögliche globale new- und delete-
Operatoren für den Linux-Kernelspace.

Auch dieses Beispiel in Listing 4 soll nur die grundsätzliche Anwendbarkeit von C++ in einem Systemkontext mit spezieller dynamischer Speicherverwaltung demonstrieren. Gerade in der Linux Treiberentwicklung gibt es jedeoch viele Gründe die für die Wahl der Sprache C sprechen. Insbesondere das vorbereitete Buildsystem und die vielen bereits vorhandenen Vorlagen in C.

Der ISO-Standard von 1998

Wie in vorangegangenen Abschnitten bereits angesprochen wurde, stellt die ANSI/ISO-Standardisierung für den Einsatz von C++ nicht unbedingt eine Vereinfachung dar. Die wesentlichen Änderungen des ISO-Standards an der Sprachsyntax von C++ gegenüber der älteren AT&T-Variante sind:

  • Die Einführung von Exception Handling in den Sprachstandard.
  • Die Neudefinition des Operators new mit Fehlerbehandlung über Exceptions.
  • Die Einführung eines weiteren new-Operators mit dem alten Verhalten aber einer neuen Signatur - nothrow Variante
  • Die Einführung von Namespaces.
  • Neue Typencovertierungsoperatoren.
  • Rudimentäre Beschreibungsmöglichkeit von Typen mit type_info.

Die Änderungen des Standards an der C++ Bibliothek sind:

  • Die Nutzung von Exception Handling in der C++ Bibliothek.
  • Die Aufnahme neuer Bibliothekselemente - insbesondere der STL.
  • Die Verwendung des Namespaces std für die Bibliothek.

Insbesondere die Punkte, die sich mit dem Thema Exception Handling befassen, werfen für den Einsatz in modernen Softwareprojekten die meisten Fragen auf. Folgende Punkte müssen immer geklärt werden:

  1. Grundsätzliche Verwendbarkeit von Exception Handling im Systemkontext der Software.
  2. Notwendigkeit der Integration von altem - nicht ISO konformen - und neuem Code.
  3. Notwendigkeit der Integration von Code der nicht Exception sicher ist.
  4. Ausbildungsstand der Mitarbeiter bezüglich des Einsatzes von Exception Handling.
  5. Verwendbarkeit von Elementen aus der Standardbibliothek.

EC++ - ein Embedded C++ Standard

EC++ ist ein von einem Konsortium japanischer Chiphersteller definierter Standard, der gegenüber C++ einen eingeschränkten Sprach- und Bibliotheksumfang aufweist. Ziel der Einschränkung war, die Sprache von allem zu befreien, was zu Ineffizienz bei Codegröße und Laufzeit führen kann. Außerdem sollte der Standard dazu führen, dass Compiler für neue embedded Plattformen möglichst einfach erstellbar sein sollen.

Spracheigenschaften, die in EC++ nicht enthalten sind:

  • Mehrfachvererbung
  • Virtuelle Basisklassen
  • Typüberprüfung zur Laufzeit
  • Die Typumwandlungsoperatoren static_cast<>, dynamic_cast<>, reinterpret_cast<> und const_cast<>
  • Das Schlüsselwort mutable
  • Namensräume
  • Exception Handling
  • Templates

Insgesamt erfährt EC++ viel Kritik aus der C++ Entwicklergemeinde, da wesentliche syntaktische Merkmale von C++ fehlen und durch aufwendigere Programmierarbeit ausgeglichen werden müssen. Es ist für einige C++ Merkmale auch nicht nachvollziehbar, warum sie in EC++ nicht gewünscht wurden. Insbesondere die Typenumwandlungs­operatoren, die Templates und die Namensräume haben keine Effizienznachteile. Insofern führt der EC++ Standard nicht wirklich zu effizienteren Programmen. Wie im Abschnitt zu Codegröße und Geschwindigkeit schon ausgeführt wurde, shließt sich der Autor dieses Artikels der genannten Kritik an. Da auch nicht sehr viele Compiler in der Lage sind, diesen Standard zu unterstützen, ist EC++ keine echte Lösung

Ein Vergleich mit Java

C++ und Java sind nicht einfach zwei Sprachen, die man in ihren syntaktischen Besonderheiten vergleichen kann. C++ ist eine Sprache, die in Hardware-spezifischen Maschinencode übersetzt wird. Java braucht eine Virtual Machine um laufen zu können. Der zu Java-Bytecode übersetzte Quelltext ist allein nicht lauffähig. In dieser VM liegt der Schlüssel zum Verständnis des Unterschieds, denn sie definiert ein Typensystem das erstens über alle Plattformen vom Mobiltelefon bist zum Großrechner identisch ist und zweitens die Typen zur Laufzeit beschreiben kann. Außerdem ist der Java-Bytecode auch auf jeder Plattform lauffähig. Diese zwei Grundeigenschaften haben weitreichende Konsequenzen. Eine Konsequenz ist die weitrechende Plattformunabhängigkeit von Java-Code. Eine andere ist die hohe Connectivity von Java als Plattform, denn durch das vereinheitlichte Typensystem und die zur Laufzeit beschriebenen Typen ist es ein Leichtes zwischen Javaprogrammen eine Verbindung aufzubauen und komplexe Informationen zu übertragen. Man überträgt einfach Objekte oder ganze Objekthierarchien. Die Java-VM ist also eine Art Betriebssystemaufsatz, eine Art vereinheitlichende Plattform. Eine solche Vereinheitlichung zwischen verschiedenen C++-Programmen auf verschiedenen Plattformen ist praktisch nicht zu erreichen. Man muss schon bei relativ einfachen Kommunikationsaufgaben große Sorgfalt auf die Typisierung der Daten verwenden. Frameworks zur Vereinheitlichung der Typensysteme zur Kommunikation gibt es einige (CORBA), sie sind jedoch alle auf eine begrenzte Anzahl von Plattformen beschränkt.
Java bringt also vor allem im Kommunikationsbereich und auch in anderen Bereichen gegenüber C++ eine wesentlich gesteigerte Produktivität für den Entwickler mit sich. Diese gesteigerte Produktivität hat auch ihre Kosten. Zum einen ist die Garbage Collection in Java durch parallele Threads realisiert, die autark arbeiten und nicht bzw. nur wenig vom Programmierer beeinflusst werden können. Diese Autarkie bedeutet, dass diese Threads zu jeder Zeit tätig werden können. Insbesondere in Codebereichen, die deterministisches Laufzeitverhalten voraussetzen, ist diese Eigenschaft der Java-VM unakzeptabel. Der nächste Punkt ist die Typenbeschreibung: jede Klasse wird in Java zur Laufzeit beschrieben. Üblicherweise werden die meisten Klassen zum Start des Programms geladen und beschrieben. Es findet etwas statt, was man entfernt mit dem Linkvorgang eines C/C++-Programms vergleichen könnte, nur dass es beim Programmstart passiert. Der Startvorgang wird dadurch extrem langsam im Vergleich zu einem C++-Programm. Die Beschreibungen der Typen benötigen außerdem Speicher zur Laufzeit. Das ist der Hauptgrund dafür, warum Java-Programme in der Regel sehr viel Hauptsspeicher konsumieren. Ob diese Kosten bezahlbar sind, muss sich am gestellten Problem entscheiden. Eine allgemeine Absage an Java im Embedded System ist sicher fehl am Platz, schließlich bekommt man einen enormen Produktivitäts­vorsprung gegenüber C++. Was man betrachen muss sind die erhöhten Kosten für die Hardware. Diese muss man mit der Anzahl der auszuliefernden Systeme multiplizieren, um diese Kosten den (geplanten) gesparten Entwicklungskosten gegenüberstellen zu können.

Ralf Schneeweiß - 04. August 2004
kleinere Korrekturen am 28. April 2021


1)

Es gibt eine geringe Anzahl von Definitionen, die für C und dem C-Anteil in C++ unterschiedlich sind. Insofern ist C++ nicht im strengen Sinne eine Obermenge der Sprache C. Da diese Definitionen aber strukturell nicht von Bedeutung sind, sollen sie hier außer Acht gelassen werden und C++ als strukturelle Obermenge von C betrachtet werden.

2)

Dem öfters von eingefleischten C-Programmierern vorgebrachten Argument, dass auch in C-Projekten eine vergleichbare Strukturierung erreicht werden kann sei zugestimmt. Es lassen sich objektorientierten Programme auch in C schreiben. Die Unterstützung durch eine OO-Sprache erleichtert jedoch die Anwendung von OO-Konstrukten.

3)

Zum Beispiel durch den Rückgriff auf die Entwurfsmuster aus [GoF95].


Literatur

[AA01] 

Andrei Alexandrescu: Modern C++ Design. Generic Programming and Design Patterns Applied. 2001.

[TR06] 

ISO/IEC TR 18015:2006(E). Technical Report on C++ Performance. 2006.

[C++03] 

ISO/IEC 14882:2003(E). International Standard Programming Languages − C++. Second edition 2003-10-15.

[GoF95] 

Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides: Design Patterns. Elements of Reusable Objectoriented Software. 1995.




Zum Anfang des Dokuments.