C++11 – auch ein Stimmungsbild

Seite 2: Top 11 der C++11-Features I

Inhaltsverzeichnis

Alle neuen C++11-Funktionen auch nur kurz skizzieren zu wollen, würde über das hinausgehen, was dieser Artikel zu leisten vermag. Deshalb seien nur die elf – nach Meinung des Autors – wichtigsten Neuerungen beschrieben:

Ohne Zweifel ist die in der Praxis wichtigste Neuerung (wenn auch schon vor Jahren mit Boost und TR1 eingeführt) std::shared_ptr, das Schweizer Taschenmesser unter den intelligenten Zeigerklassen. Das mag angesichts von Funktionen mit deutlich mehr "Sexappeal" verwundern, jedoch hat sich shared_ptr als unverzichtbares Ausdrucksmittel in der C++-Entwicklung erwiesen und ist wahrscheinlich für eine ganze Generation von C++-Entwicklern die "Einstiegsdroge" für Boost gewesen.

Es erhält seine Universalität durch den sogenannten "Custom Deleter", der, als zweites Konstruktorargument angegeben, nicht Teil des shared_ptr-Typs ist. Mit seiner Hilfe kann man den standardmäßigen delete-Aufruf zum Zerstören des Objekts durch selbst definierte Funktionen ersetzen. So legt der Entwickler zum Beispiel durch Angabe von fclose als Custom Deleter einen shared_ptr<FILE> an. Übergibt er eine leere Funktion, kann er das Zerstören sogar ganz verhindern, um beispielsweise static-Objekte wie Heap-Objekte zu behandeln.

Aber auch ohne explizite Angabe des Deleter-Arguments zeigt sich ein positiver Effekt: Es lässt sich eine Klasse ohne virtuellen Destruktor problemlos durch einen shared_ptr der Basisklasse zerstören (etwas, das bei nackten Zeigern sofort "undefiniertes Verhalten" auslöst), oder unter Windows eine Klasse in einer Debug-DLL erzeugen und in einer Release-DLL zerstören (ohne shared_ptr erhält man eine Heap-Korrumpierung), denn der Zerstörungscode läuft dank des Custom Deleter in der Debug-DLL.

shared_ptr<FILE> file( fopen( "file.txt", "rw" ), &fclose );
fwrite( "hi", 1, 2, file.get() ); // conversion to FILE* requires explicit get()
//..
// calls fclose( file.get() ) when last shared_ptr goes out of scope

Aus der Riege der wirklichen Neuerungen sticht auto hervor (Boost.Typeof, der Versuch einer reinen Bibliotheksumsetzung ohne explizite Sprachunterstützung, war nicht besonders anwenderfreundlich). Denn die Compiler enthalten den Code hierfür bereits (für den sizeof-Operator), es fehlte nur an einer einheitlichen Schnittstelle für den Programmierer. Mit auto und decltype steht diese nun bereit. Während decltype (typeof war durch zu viele inkompatible Erweiterungen der C++-Compiler-Bauer belegt, daher der etwas kryptische Name) wie sizeof ein Operator ist, der sich überall dort einsetzen lässt, wo ein Typ erwartet wird, verwendet man auto in Variablendefinitionen mit direkter Zuweisung, um sich die Angabe des genauen Typs der Variablen zu sparen:

auto i = 0; // i is an 'int'
const auto d = 0.0; // d is a 'const double'
auto b = bind( &func, _1, _2, 13 ); // 'b' cannot
// (portably) be declared!
for ( auto it = vec.begin() ; it != vec.end() ; ++it )

Daneben leitet auto auch die neue alternative Syntax für Funktionsdeklarationen ein, die sich im Gegensatz zur klassischen Variante dadurch auszeichnet, dass Zugriff auf die formalen Parameter besteht. Besonders interessant ist das ist das bei Funktions-Templates, denn dann ist die Angabe des Typs des Return-Werts meist nicht mehr so einfach möglich, vor allem, wenn die komplizierten C-Ganzzahl-Konvertierungsregeln abzubilden sind:

auto add( int a, int b ) -> int { return a + b; }
template <typename T, typename S>
auto add( T a, S b ) -> decltype(a+b) { return a + b; }

Bei Lambda-Funktionen (siehe unten) ist die Angabe des Rückgabetyps sogar optional, wenn die Funktion lediglich aus einer Rückgabeanweisung besteht. Das ist leider bei normalen Funktionen nicht erlaubt.

Neu ist die Möglichkeit, Datenfelder von Klassen direkt bei der Deklaration initialisieren zu können. Das ist besonders nützlich bei Zeigervariablen, denen man nun an Ort und Stelle Null zuweisen kann:

class Class {
// ...
private:
MyWidget * m_myWidget = 0;
double d = 1.0;
};

Die so angegebenen direkten Initialisierungswerte verwenden alle Konstruktoren, sie lassen sich in den Initialisierungslisten einzelner Konstruktoren aber dennoch individuell ersetzen.

Auch wenn die beiden Funktionen nicht deckungsgleich sind, sollen sie zusammengefasst werden, denn sie verfolgen das gleiche Ziel, die Anpassung von Standardalgorithmen an benutzerspezifische Situationen zu vereinfachen. Gemeint sind die zahlreichen Funktions- oder Prädikatparameter der "Standard Template Library"-Algorithmen (STL), zu deren effektiver Nutzung man bisher händisch Funktionsobjekte schreiben musste (bind1st und bind2nd sollen nur der Vollständigkeit halber erwähnt werden; ihre Verwendung stößt in der Praxis selbst bei sparsamer Anwendung schnell an Lesbarkeitsgrenzen).

Während bind() die im Standard enthaltenen bind1st und bind2nd durch eine einheitliche und deutlich nutzerfreundlichere Syntax ersetzt, sind Lambda-Funktionen eine neue Sprachfunktion, mit deren Hilfe sich kleine Funktionen an Ort und Stelle definieren lassen. Leider ist die Syntax etwas gewöhnungsbedürftig geraten, und wirklich kleine Funktionen werden durch die Argumentdeklaration sehr länglich (hier glänzt Boost.Lambda, eine Implementierung auf Bibliotheksebene, durch willkommenen Minimalismus). Nichtsdestoweniger sind Lambda-Funktionen ein wichtiger Baustein und eine willkommene Erleichterung für STL-affine Entwickler.

Hier nun ein Beispiel (vorher/nachher):

int f( int );
int g( int );
// int x-> f( g( x ) ) and int x -> ( x + 1 ) * 2 as functors:
// C++98:
compose1( ptr_fun( f ), ptr_fun( g ) )
bind2nd( multiplies<int>, bind2nd( plus<int>(), 1 ), 2 )
// C++0x bind()
bind( f, bind( g, _1 ) )
bind( multiplies<int>(), bind( plus<int>(), _1, 1 ), 2 )
// C++-0x lambdas
[] ( int x ) -> int { return f( g( x ) ); } // -> int is optional
[] ( int x ) { return ( x + 1 ) * 2; }
// Boost.Lambda
bind( f, bind( g, _1 ) )
( _1 + 1 ) * 2

Nicht nur für die HPC-Fraktion (High Performance Computing), sondern auch Embedded-Entwicklern und Programmierern, die gern statische, konstante Tabellen verwenden, um Daten und Code zu trennen, kommt constexpr wie gerufen. D-Programmierer werden dahinter ihr lieb gewonnenes pure-Schlüsselwort wiedererkennen, auch wenn die C++-Variante einen etwas anderen Ansatz verfolgt.

Eine Funktion ist „pur“, wenn sie allein von tatsächlichen Parametern abhängt, also nicht nur seiteneffektfrei ist, sondern auch keinerlei nichtkonstanten globalen Zustand verwendet. Eine C++-Funktion lässt sich als constexpr deklarieren, wenn sie „pur“ ist und nur aus einer Rückgabeanweisung (return statement) besteht, die selbst eine constexpr ist. Besonders interessant wird die Funktion dadurch, dass auch Konstruktoren constexpr sein können, wenn sie nur aus der Konstanten-Initialisierungsliste bestehen (also einen leeren Rumpf haben) und die Ausdrücke in der Initialisierungsliste constexpr sind. Ein Beispiel zeigt das folgende Listing.

class Point {
int px, py;
public:
constexpr Point() : px(0), py(0) {}
constexpr Point( int x, int y ) : px(x), py(y) {}
constexpr int x() const { return px; }
constexpr int y() const { return py; }
};
class Rect {
int x1, y1, x2, y2;
public:
constexpr Rect() : x1(0), y1(0), x2(0), y2(0) {}
constexpr Rect( int x, int y, int w, int h )
: x1(x), y1(y), x2(x+w-1), y2(y+h-1) {}
constexpt Rect( const Point & p1, const Point & p2 )
// ...provided std::{min,max} are constexpr...
: x1( std::min( p1.x(), p2.x() ) ),
y1( std::min( p1.y(), p2.y() ) ),
x2( std::max( p1.x(), p2.x() ) ),
y2( std::max( p1.y(), p2.y() ) ) {}
// ...
};
constexpr Rect rects[] = {
Rect( 0, 0, 100, 100 ),
Rect( Point( -10, -10 ), Point( 10, 10 ) ),
};

Die durchgängige constexpr-Kette erlaubt dem Compiler, das rects-Feld zur Übersetzungszeit zu berechnen, und dem Linker, es im Nur-Lese-Speicher abzulegen. Der Autor erwartet, constexpr in seinen Projekten ähnlich oft zu verwenden wie das klassische const: nämlich überall dort, wo es zulässig ist.

Zum Schluss sei noch auf den fundamentalen Unterschied zu Ds pure hingewiesen: Während pure dem D-Compiler Informationen aus anderen Übersetzungseinheiten zur Verfügung stellt, um CSE (Common Subexpression Elimination) zu ermöglichen, ist constexpr genau genommen redundant. Ein guter Optimierer würde den oben beschriebenen Effekt auch ohne constexpr bewerkstelligen, zumal der Standard verlangt, dass Rümpfe von constexpr-Funktionen in jeder Übersetzungseinheit sichtbar sind. Der Vorteil des Schlüsselworts liegt jedoch darin, dass der Programmierer dem Compiler damit seine Intention vorgeben kann und eine Rückmeldung (Fehler) erhält, wenn der Compiler herausfindet, dass er der Vorgabe nicht folgen kann.