iX 4/2017
S. 92
Wissen
Programmiersprachen
Aufmacherbild

C++17 – was der neue Standard bringt

In Erwartung

Noch ist C++17 nicht offiziell verabschiedet. Aber welche Features es in den neuen Standard schaffen werden und welche nicht, steht mittlerweile fest. Und auch, auf welche Neuerungen die C++-Gemeinde in drei Jahren für den kommenden großen Standard C++20 hoffen darf.

Die Erwartungen der C++-Community waren hoch – zu hoch. Die Änderungen in C++17 sind in ihrer Tragweite nicht mit denen von 2011 zu vergleichen. Zwar bringt der neue Standard einige großartige Features mit, auf die lang ersehnten Concepts Lite, die Ranges-Bibliothek und verbesserte Futures muss die Community aber noch mindestens drei Jahre warten.

Allerdings hat sich die anfängliche Enttäuschung spätestens seit dem Treffen des XpertC++-Standardisierungskomitees im letzten Jahr gegeben. Denn nun stehen neben wichtigen Neuerungen wie einer Bibliothek fürs Dateisystem oder einer parallelisierten Version der STL-Algorithmen auch wichtige Modifikationen der C++-Kernsprache an.

Die nahe Zukunft: C++17

Die Evolution von C++ geht weiter. Die Verbesserungen der Kernsprache finden auf allen Ebenen statt: zur Compile-, aber auch zur Laufzeit. Die Benutzerfreundlichkeit der Sprache wird verbessert, indem bestehende Konzepte abgerundet und alte Zöpfe abgeschnitten werden. Aber auch für Freunde der Optimierung ist etwas dabei.

Los geht es mit der Compile-Zeit. Hier stehen die fold expressions und constexpr if auf der Habenseite. Der iX-Artikel zur funktionalen Programmierung mit C++17 [1] stellte die fold expressions bereits detailliert vor. Daher an dieser Stelle der Vollständigkeit halber die Fakten im Stakkato-Takt.

Listing 1: Variadische Templates in Kombination mit fold expressions

1  #include <iostream>
2  
3  template<typename... Args>
4  bool all(Args... args) { return (... && args); }
5  
6  int main(){
7  
8    std::cout << std::boolalpha;
9  
10   std::cout << "all(): " << all() << std::endl;
11   std::cout << "all(true): " << all(true) << std::endl;
12   std::cout << "all(true, true, true, false): " << all(true, true, true, false) << std::endl;
13 
14   std::cout << std::endl;
15 }
Berechnung des Wahrheitswertes mit fold expressions (Abb. 1)

C++11 kennt variadische Templates [a] (siehe „Alle Links“ am Ende des Artikels), also solche mit einer beliebigen Anzahl von Parametern. Diese beliebige Anzahl wird im Parameter Pack gehalten. Neu ist mit C++17, dass man ein Parameter Pack direkt über einen binären Operator reduzieren kann.

Das Funktions-Template all in Listing 1 gibt zur Compile-Zeit genau dann true zurück, wenn alle Argumente wahr sind. all wendet variadische Templates in Kombination mit fold expressions an. Der Ausdruck (… && args) reduziert den Parameter Pack (…) direkt über einen binären Operator. Abbildung 1 zeigt das Ergebnis der Programmausführung.

Für die Programmausführung kommt in diesem Artikel entweder ein GCC 6.1 oder ein Clang 3.8 auf cppreference.com zum Einsatz (siehe etwa http://en.cppreference.com/w/cpp/language/fold). Die Webseite erweist sich darüber hinaus als gute Quelle, wenn man die Details zu fold expressions nachlesen möchte.

C++17 kennt zwei Varianten der fold expressions. Zum einen können sie abhängig vom binären Operator einen Default-Wert haben, zum anderen den Parameter Pack von links oder rechts beginnend verarbeiten. Unterstützt werden 32 binäre Operatoren in fold expressions: + – * / % ^ & | = < > << >> += -= *= /= %= ^= &= |= <<= >>= == != <= >= && || , .* ->* .

Bedingtes Kompilieren mit constexpr if

Listing 2: Bedingtes Kompilieren mit constexpr

1  template <typename T>
2  auto getValue(T t) {
3    if constexpr (std::is_pointer_v<T>)
4      return t;  // deduces return type to int* for T = int*
5    else
6      return t;  // deduces return type to int for T = int
7  }

Mit constexpr if hat C++17 zur Compile-Zeit noch mehr zu bieten. Der Ausdruck erlaubt es, Quellcode bedingt zu übersetzen. Dabei muss der zu evaluierende Ausdruck selbst einer sein, der sich zur Compile-Zeit auswerten lässt und einen Wahrheitswert zurückgibt. Nutzt man auto, um automatisch den richtigen Rückgabewert abzuleiten, kann man Funktions-Templates schreiben, die verschiedene Datentypen zurückgeben. Eine Regel gilt es im Kopf zu behalten: Der Sourcecode, der nicht kompiliert wird, muss syntaktisch richtig sein. Listing 2 zeigt die wichtigsten Fakten in der Anwendung.

Falls T ein Zeiger ist, wird der if-Zweig des Funktions-Templates übersetzt, falls nicht, der else-Zweig. Daher ist im ersten Fall der Rückgabetyp ein Zeiger, im zweiten hingegen ein einfacher Wert. Der Begriff Evolution passt vor allem auf die Features in C++17, die das bestehende Funktionsspektrum von C++14 abrunden.

if und switch mit Initialisierern

Klein, aber fein – so lassen sich die neuen if- und switch-Anweisungen mit Initialisierern charakterisieren. Wie die klassische for-Schleife ((for int i= 0; i <= size; ++i) kann man Variablen damit direkt definieren. Daher ist es nicht mehr nötig, den umgebenden Bereich durch eine deklarierte Variable zu verschmutzen. Genau das passiert im klassischen Fall, wenn ein neues Paar in eine std::map eingefügt wird:

std::map<int,std::string> myMap;
auto result = myMap.insert(value);
if (result.second){
   useResult(result.first);
   // … 
} 
else{
   // … }

Dies wird mit der neuen Syntax deutlich eleganter. Zum einen wird die Variable result nicht in den umgebenden Bereich eingefügt. Zum anderen wird sie automatisch am Ende der if-Anweisung gelöscht:

std::map<int,std::string> myMap;
if (auto result = myMap.insert(value);
   result.second){
   useResult(result.first);
   // …
} 
else{
   // … 
}  // automatisches Löschen von result

myMap.insert gibt ein Paar zurück. result.first ist ein Iterator auf das eingefügte Paar und result.second gibt Auskunft darüber, ob das Einfügen erfolgreich war. Dabei ist result auch im else-Zweig gültig.

Die großen C++-Standards: Die „kleinen“ C++-Standards C++03 und C++14 fehlen in dieser Übersicht (Abb. 2).

Das ist aber noch nicht das Ende der Verbesserungen, die C++17 bringt. Verwendet man die if-Anweisung mit Initialisierer in Kombination mit Structured Binding, ist der Code noch besser lesbar. Diese strukturierte Bindung erlaubt es, ein std::tuple oder ein struct direkt an Variablen zu binden. Die Variablen müssen noch nicht vorhanden sein:

std::tuple<T1,T2,T3,T4,T5> getValues(){ ... }
auto [a, b, c, d, e] = getValues(); 
// types are: T1, T2, T3, T4, T5

Damit werden die Variablen a bis e erzeugt, die die Werte des Funktionsaufrufs getValues haben. Durch auto wird der Typ automatisch bestimmt. Mit strukturierter Bindung ergibt sich die endgültige Fassung der if-Anweisung mit Initialisierer:

std::map<int,std::string> myMap;
if (auto [iter, succeeded] =  
 myMap.insert(value);
   succeeded) {
   useIter(iter);
   // … 
} 
else{
   // … 
} 

iter und succeeded werden automatisch gelöscht. Funktions-Templates sind deutlich benutzerfreundlicher als bisherige Klassen-Templates, da sie aus den Funktionsargumenten die Typen des Templates ableiten können. Jetzt können das auch Konstruktoren von Klassen-Templates.

Automatische Typableitung bei Klassen-Templates

Listing 3: Automatische Typableitung für Konstruktoren von Klassen-Templates

1  #include <iostream>
2  
3  template <typename T>
4  void showMe(const T& t){
5    std::cout << t << std::endl;
6  }
7  
8  template <typename T>
9  struct ShowMe{
10   ShowMe(const T& t){
11     std::cout << t << std::endl;
12   }
13 };
14 
15 int main(){
16   
17   std::cout << std::endl;
18     
19   showMe(5.5);          // not showMe<double>(5.5);
20   showMe(5);            // not showMe<int>(5);
21     
22   ShowMe<double>(5.5);  // with C++17: ShowMe(5.5);
23   ShowMe<int>(5);       // with C++17: ShowMe(5);
24   
25   std::cout << std::endl;
26     
27 }

Da sich ein Funktions-Template aus Benutzersicht wie eine Funktion verhält, gestaltet sich das Ableiten der Datentypen aus seinen Funktionsargumenten ganz einfach. Die Zeilen 19 und 20 von Listing 3 zeigen die automatische Typableitung bei Funktions-Templates in Aktion. So ist es nicht notwendig, den expliziten Typ von showMe(5.5) mit dem Template-Argument double zu verwenden (showMe<double>(5.5)). Der Compiler kann den Typ double aus dem Typ des Funktionsarguments 5.5 automatisch ableiten. Daher gestaltet sich der Aufruf der Funktions-Templates wie der Aufruf einer normalen Funktion. Das wird mit C++17 auch für Klassen-Templates möglich, sodass ShowMe<double>(5.5) einfach als ShowMe(5.5) (Zeile 22) geschrieben werden kann.