C++0x:
die Highlights des kommenden ISO - C++ - Standards
von Kurnia Hendrawan
Der kommende ISO - C++ - Standard heißt C++0x. Das für die Entwicklung der Sprache zuständige Komitee hat den letzten Working Draft N3126 Ende August 2010 mit dem Ziel veröffentlicht, möglichst viele Feedbacks von Entwicklern sammeln zu können. Ursprünglich wurde der Name C++0x von seinem Erfinder Bjarne Stroustrup in der Hoffnung gewählt, dass dieser neue Standard im Jahr 2008 oder 2009 verabschiedet werden könnte. 0x steht folglich für das Jahr der Veröffentlichung. Die Entwicklung hat sich jedoch verzögert und die finale Version wird nicht vor 2011 erwartet.
Auch bei consistec verfolgen wir kontinuierlich die Entwicklung dieser Programmier-sprache. Die am meisten diskutierten Highlights fassen wir hier für Sie zusammen und setzen uns dabei kritisch mit ihnen auseinander. Dabei beziehen wir uns auf die Informationen aus dem oben genannten Working Draft, dessen Inhalt vermutlich sehr nah an der finalen Version des Standards ist.
Objekt-Einschränken durch delete
Das Schlüsselwort delete wird in C++ für Destruktoren verwendet – der neue Standard erweitert diese Funktionalität nun auf beliebige Member-Funktionen. Dadurch kann deren Aufruf entweder generell oder für bestimmte Operatoren verhindert werden.
Beispiel:
class CNoCopy{
CNoCopy & operator=(const CNoCopy) = delete;
CNoCopy(const CNoCopy&) = delete;
CNoCopy() = default;
};
struct SNoNew{
SNoNew * operator new
(std::_size_t) = delete;};
struct SOnlyInt{
void f(int aValue);
template<class T> void f(T)
= delete;};
Nichtkopierbare Klassen – wie CNoCopy im obigen Beispiel – sind unter anderem beim Umgang mit Ressourcen (Dateien, Locks, Netzwerkverbindungen) sinnvoll. Wird das Erzeugen eines Objektes durch den new-Operator verhindert, muss das Objekt auf dem Stack oder als Mitglied eines anderen Objektes alloziert werden. Das letzte Beispiel schließlich zeigt die Möglichkeit, beim Aufruf von Member-Funktionen stillschweigende Typkonvertierungen zu verhindern, die in der Praxis regelmäßig zu Fehlern führen.
Delegation-Konstruktoren
C++0x erlaubt das Aufrufen von Konstruktoren innerhalb anderer Konstruktoren. Diese sogenannten Delegation-Konstruktoren ersetzen zukünftig die fehleranfällige init()-Methode.
Beispiel:
class CDelCons{
int value;
public:
CDelCons(int v) : value(v) {}
CDelCons() : CDelCons(17) {}
};
Unsere Einschätzung: Ähnlich wie die Erweiterung von delete dürften die Delegation-Konstruktoren eher selten zum Einsatz kommen. Benötigt ein Entwickler jedoch gerade diese Funktionalität, bietet sie ihm eine sehr elegante Lösung, um das Duplizieren von Code zu vermeiden.
Konstruktoren-Vererbung
Mit Einführung des neuen Standards können nicht nur Mitglieder einer Klasse vererbt werden, sondern auch die Konstruktoren der Basisklasse.
Beispiel:
class CBase{
public:
CBase(int i);
};
class CDerived : public CBase{
public:
using CBase::CBase;
};
Unsere Einschätzung: Diese neue Möglichkeit steigert die Wiederverwendbarkeit von Code deutlich. Dies ermöglicht letztlich eine elegante und weniger fehleranfällige Programmierung.
Initializer_list
Mithilfe des Features initializer_list können statische Listen erstellt oder eine Struktur mit bestimmten Werten initialisiert werden.
Beispiel:
class CNewVector{
public:
CNewVector
(std::initializer_list<int> list);};
CNewVector newVector = {4, 7, 11};
Der {}-Initializer kann zudem für viele Funktionen in der Standardbibliothek verwendet werden, wie etwa:
a = std::min({21, 91, 96, 7});
Unsere Einschätzung: Auf den ersten Blick scheint das Feature eine nette Erweite-rung zu sein. In der Praxis wird es jedoch – obwohl es insbesondere in Testcode deutlich Schreibarbeit erspart – in einem komplexen Programm kaum eingesetzt werden, da Container nur selten direkt mit Werten initialisiert werden.
Rvalue-Referenz und move-Semantik
Rvalue-Referenzen – deklariert mit T&& statt T& – können verwendet werden, um das Kopieren der Objektkomponenten bei temporären Objekten oder beim Rückgabewert einer Funktion zu vermeiden. Auf diese Weise lässt sich ein Objekt effizient an Funktionen weiterleiten. Dies war im bisherigen C++ nicht ohne Weiteres möglich.
Die move-Semantik transferiert Inhalte aus einem bestehenden Objekt beim Erzeugen eines neuen Objekts automatisch, anstatt wie bisher eine teure Kopie zu erstellen. Das normale Kopieren „v1 = v2“ wird bei Objekten durch mehrere Schritte durchgeführt: Funktionsaufruf, Speicherallokation und Schleifen zum Kopieren der Elemente. Dies ist nur akzeptabel, wenn man tatsächlich zwei Kopien haben möchten. In den meisten Situationen will man dies jedoch vermeiden.
Beispiel (Rvalue-Referenz):
std::vector<int> mCreateVector(int aLength){
std::vector<int> v;
for (int i = 0; i <
aLength, ++i){v.push_back(i+1);}return v;
}
std::vector<int>&& v = mCreateVector(5);
Ein weiteres Anwendungsszenario im Bezug auf Objektkonstruktion lautet:
CNewVector& v1 = CNewVector(); // Fehler beim Kompilieren!!
CNewVector&& v2 = CNewVector(); // OK
Unsere Einschätzung: Der Umgang mit Containern beeinflusst die End-Performance eines Programms oftmals dramatisch. Durch geschickte Verwendung von Rvalue-Referenzen lassen sich jedoch viele unnötige Kopien von Objekten vermeiden.
Typ-Inferenz
In der modernen Softwareentwicklung wird die Template-Metaprogrammierung immer populärer. Sie erschwert es jedoch bisweilen, den Typ einer Variablen manuell zu deklarieren. Je nach Anwendung bietet C++0x nun gleich zwei Möglichkeiten, den Typ erst beim Kompilieren (automatisch) festzulegen. Die Schlüsselwörter sind auto und decltype.
Während auto anstelle eines Typs verwendet wird, benötigt decltype ein Argument, dessen Typ dann auch als Typ für die Variable während des Kompilierens bestimmt wird. Anders als bei decltype, ist der Typ einer auto Variablen nur dem Compiler bekannt.
Beispiel (auto):
for (auto itr = v.begin(); itr != v.end(); ++itr)
Beispiel (decltype):
int& foo(int& i);
float& foo(float f);
decltype(foo(t)) a = foo(t);
Hier müsste der Typ von a bei Änderungen des Typs von t ebenfalls angepasst werden. Mit decltype kann kann der Compiler diese Aufgabe übernehmen und der Programmieraufwand wesentlich reduziert werden.
Unsere Einschätzung: Für bestimmte Szenarien dürften die beiden neuen Schlüssel-
wörter sehr hilfreich sein. Außerdem kann auto für einen sauberen und leicht verständlichen Code sorgen.
Range-basierte for-Schleife
Beispiel:
//for (std::vector<int>::
//const_iterator itr = v.begin();
//itr != v.end(); itr++){for (auto itr : v){
std::cout << *itr << “ “;
};
std::cout << “\n“;
Die auskommentierte Zeile zeigt die Syntax des aktuellen C++, womit man einen Container per for-Schleife durchläuft.
Unsere Einschätzung: Range-basierte for-Schleifen erleichtern Entwicklern die Arbeit
deutlich. Sie sorgen für Zeitersparnis und für übersichtlichen Code, reduzieren das Fehlerrisiko und vereinfachen Refactorings deutlich. Die alternative Schreibweise wird in der Praxis daher sehr gerne und häufig Anwendung finden.
Lambda-Funktion
Mehrere Algorithmen (z. B. sort und find) aus der Standardbibliothek benötigen eine Prädikat-Funktion, die separat definiert werden muss. Jedoch werden diese Funktionen meist nur für den entsprechenden Algorithmus verwendet und dann schnell wieder vergessen. In Zukunft kann sich der Programmierer die Funktionsdeklaration dank der Lambda-Funktionen sparen.
Die Syntax von Lambda-Funktionen in C++0x lautet wie folgt:
[<Bindung>](<Argumente>)
{<Funktionskörper>}
Die Bindung gibt an, wie die Lambda-Funktion auf Variablen zugreift. Sie kann leer gelassen werden (kein Zugriff auf äußere Variablen), aber auch das Zeichen „&“ (lokale Variablen werden referenziert) oder „=“ (lokale Variablen werden kopiert) enthalten. Zudem können auch explizit bestimmte Variablen gebunden werden: [&myVar].
Beispiel:
int total = 0;
std::for_each(v.begin(),
v.end(), [&](int aValue){total += aValue;});std::cout << total;
Der Code summiert die Elemente von v mithilfe der Lambda-Funktion. Die Kommunikation mit dem äußeren Code erfolgt hier über die Bindung an die lokale Variable total.
Unsere Einschätzung: Diese Alternative ist in vielen Fällen sicherlich sehr angenehm. Sie ist jedoch mit Vorsicht zu benutzen, da der Einsatz von Lambda-Funktionen für komplexe Aufgaben schnell zu Unübersichtlichkeit führen kann.
Variable Anzahl von Argumenten
C++0x erlaubt endlich die Definition von Templates mit einer variablen Anzahl von Argumenten, unabhängig davon, ob sie alle den gleichen Typ haben. Diese Erweiterung eröffnet eine Vielzahl an Möglichkeiten bei rekursiven Definitionen, wie zum Beispiel Tupel und Funktionsaufruf. Dieses Feature wird durch den Operator „...“ direkt nach dem Schlüsselwort typename oder dessen Instanzen realisiert.
Beispiel:
void printf(const char *pA){
while (*pA){
if (*pA == ‚%‘ && *(++pA) != ‚%‘)
throw std::runtime_error
(“invalid format string: missing argument(s)“);std::cout << *pA++;
}
}
template<typename T, typename...
Args>void printf(const char* pA, T aValue, Args... aArgs){
while (*pA){
if (*pA == ‚%‘ && *(++pA) != ‚%‘){
std::cout << aValue;
printf(pA, aArgs...);
return
}
std::cout << *pA++;
}
throw std::logic_error(“extra argument provided to printf“);
}
Der obige Beispielcode ersetzt die reguläre printf-Funktion. Die Rekursion in der unteren Funktion wurde dazu mithilfe des „...“-Operators implementiert. Die Argumente aValue und aArgs bilden zusammen das „Head and Tail“-Modell, wobei das Letztere leer sein kann (Anmerkung: Der Code ist unvollständig, da die einzelnen Überprüfungen der Spezifikationssymbole noch fehlen).
Unsere Einschätzung: Templates mit einer variablen Anzahl von Argumenten erlauben eine saubere und effiziente Implementierung typsicherer Funktionen im Stile von printf, die sogar mit nichttrivialen Objekten zurechtkommen. In der Praxis wird man diese Art Templates wohl nicht ständig benötigen, sie können aber viele früher übliche Hacks unnötig machen.
enum-Klasse
In der aktuellen Fassung bereitet der Aufzählungstyp enum einige Probleme: Die Werte werden gezwungenermaßen zu Integer konvertiert und sind in den umgebenden Scopes gültig. Durch die fehlende Typsicherheit sind (unbeabsichtigte) Vergleiche mit Integer-Werten möglich und die Sichtbarkeit im umgebenden Scope verhindert, dass zwei Aufzählungen Elemente mit gleichem Namen enthalten. Des Weiteren ist der für die Aufzählung benutzte Typ vom Compiler abhängig und kann nicht vorgegeben werden. Das folgende Beispiel in der C++0x - Umgebung verdeutlicht den Unterschied zwischen den beiden Versionen.
Beispiel:
enum eColor{white, black, red, green, blue}; //aktuelle enum
enum class eAlert{red, yellow,
green}; //C++0x
int a1 = black;
//OK wegen integer-Konvertierungint a2 = yellow;
//Fehler in C++0x, nicht im Scope!
int a3 = eColor::black;
//OK in C++0xint a4 = eAlert::yellow;
//Fehler, keine eAlert->int
Konvertierung
In diesem Beispiel hat eColor die Größe eines Integers. Es können jedoch auch andere Typen verwendet werden, um eventuell Speicherplatz zu sparen:
enum class eSmall:char{white, black, red, green, blue};
Unsere Einschätzung: Die neue Version von enum geht in die richtige Richtung, d. h. hin zu sauberer Entwicklung. Gut gefällt uns ebenso, dass die alte Version nach wie vor erlaubt ist. Denn sie dürfte von Fall zu Fall noch die bessere Wahl darstellen, wenn man z. B. für die Deklaration von Fehlercodes eines Programms eine Repräsentation der enum-Werte als Zahlen explizit benötigt.
Fazit
C++ - Programmierer dürfen sich auf das kommende C++0x freuen, da es viele Neuerungen verspricht, die in einer solch etablierten Programmiersprache wie C++ schon lange vermisst wurden. Wir sind zuversichtlich, dass das C++ - Komitee dann die richtige Entscheidung treffen wird, damit sich das verspätete Erscheinen der finalen Version am Ende doch gelohnt hat.
Quelle:
„Working Draft, Standard for Programming Language C++“, Ver.: N3126, 21.08.2010
http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2010/n3126.pdf
Artikel von Kurnia Hendrawan
zurück zur Artikel-Übersicht
