Aufgrund der flotten Updates bei den C++-Standards haben Compilerbauer in letzter Zeit alle Hände voll zu tun. Doch wie genau folgen sie den Standards und wo liegen eigentlich die Unterschiede?
Dass die ruhigen Zeiten für C++ endgültig vorüber sind, lässt ein Blick auf die Halbwertszeiten neuerer C++-Standards erahnen. Lag zwischen den zwei C++-Standards C++98 und C++11 noch eine (gefühlte) Ewigkeit (13 Jahre), hat sich der Takt mit C++11, C++14, C++17 und C++20 auf drei Jahre reduziert. Zugleich setzt das C++-Standardisierungskomitee diesen Takt mit viel Elan um.
C++-Entwicklern stellt sich hierbei unter anderem die Frage: Wie halten die Compiler-Entwickler Schritt damit? Eine Antwort lautet: Indem sie online gehen. Der Artikel stellt einige der bekanntesten Compiler vor, online und offline. Und er erklärt dabei, welcher Compiler welchen C++-Standard unterstützt (siehe auch Kasten “Segen der Vielfalt”).
Segen der Vielfalt
Der eine oder andere mag sich fragen, warum es so viele verschiedene Compiler gibt. Weil so viele Computerarchitekturen existieren, stimmt nur zur Hälfte, da auf der x86-Architektur zum Beispiel alle drei großen Compiler laufen.
Das gewichtigste Argument ist wohl, dass jeder der drei einen besonderen Bereich hat, in dem er glänzt. So punktet der Clang-Compiler mit verständlichen Fehlermeldungen und einer offenen Architektur, auf der sich viele mächtige Werkzeuge entwickeln lassen. Den GCC zeichnet aus, dass er die meisten Hardware-Architekturen unterstützt und konsequenterweise in der Embedded-Welt mit der GNU ARM Embedded Toolchain [4] zunehmend mehr Gewicht erhält. Für den MSVC gilt hingegen die Integration in Visual Studio für viele C++-Entwickler als großer Mehrwert.
Letztlich ist diese Vielfalt jedoch auch ein Segen für alle C++-Entwickler, denn Konkurrenz belebt bekanntlich das Geschäft. Jeder der drei Compiler setzt in seinem Bereich Maßstäbe und legt damit zugleich die Latte für die anderen beiden Compiler höher. Dass die Compiler im Wettlauf um die Gunst der Entwickler fortwährend neu in den Ring steigen müssen, kann diese am Ende nur erfreuen.
Den Fokus richtet er dabei auf die populären Compiler GCC [1], Clang [2] und MSVC [3] sowie die C++-Standards C++11, C++14 und C++17. Das ist keine wirkliche Einschränkung, setzen doch geschätzt 95 Prozent aller C++-Entwickler auf einen der drei genannten Übersetzer. Zugleich spart sich der Artikel den Blick zurück (C++98) und in die Zukunft (C++20).
Der Zeitstrahl in Abbildung 1 zeigt der Vollständigkeit halber die bestehenden C++-Standards, wobei er C++03 ausnimmt, das 2003 lediglich als Bugfix für C++98 erschien.
Welche Sprache spreche ich?
Doch welche Compilerversion unterstützt nun welchen C++-Standard? Diese Frage lässt sich für C++11, C++14 und C++17 sehr einfach beantworten, die wichtigsten Informationen zeigt Tabelle 1. Sie beschreibt, ab welcher Version die Compiler den betreffenden C++-Standard unterstützen.
| C++-Compiler | C++11 | C++14 | C++17 |
|---|---|---|---|
| GCC | 4.8.1 | 5.0 | 7 |
| Clang | 3.3 | 3.4 | 5 |
| MSVC | 19.0 | 19.1 | 19.1(teilweise) |
Die Tabelle bezieht sich im Wesentlichen auf die Unterstützung der C++-Kernsprache und benötigt zumindest eine Erläuterung zu MSVC. Dessen Version 19.0 ist Bestandteil von Microsoft Visual Studio 2015, MSVC ist Bestandteil von Microsoft Visual Studio 2017. MSVC 19.1 unterstützt C++17 allerdings nur teilweise, zudem erfordert der Support das dritte Update. Die Tabellen 2 und 3 zeigen noch einmal alle Details zu C++17 in einer Übersicht [5].
| C++17 Features | Paper | Version | GCC | Clang | MSVC |
|---|---|---|---|---|---|
| New auto Rules for Direct-list-initialization | N3922 | c++17-lang | 5 | 3.8 | 19.0 |
| »static_assert« with no Message | N3928 | c++17-lang | 6 | 2.5 | 19.1 |
| Typename in a Template Template Parameter | N4051 | c++17-lang | 5 | 3.5 | 19.0 |
| Removing Trigraphs | N4086 | c++17-lang | 5.1 | 3.5 | 16.0 |
| Nested Namespace Definition | N4230 | c++17-lang | 6 | 3.6 | 19.0 |
| Attributes for Namespaces and Enumerators | N4266 | c++17-lang | 4.9 (Namespaces), 6 (Enumerators) | 3.6 | 19.0 |
| »u8« Character Literals | N4267 | c++17-lang | 6 | 3.6 | 19.0 |
| Allow Constant Evaluation for all non-type Template Arguments | N4268 | c++17-lang | 6 | 3.6 | no |
| Fold Expressions | N4295 | c++17-lang | 6 | 3.6 | no |
| Remove deprecated use of the Register Keyword | P0001R1 | c++17-lang | 7 | 3.8 | 19.1 |
| Remove deprecated »operator++« (Bool) | P0002R1 | c++17-lang | 7 | 3.8 | 19.1 |
| Removing deprecated Exception Specifications from C++17 | P0003R5 | c++17-lang | 7 | 4 | no |
| Make Exception Specifications Part of the Type System | P0012R1 | c++17-lang | 7 | 4 | no |
| Aggregate Initialization of Classes with Base Classes | P0017R1 | c++17-lang | 7 | 3.9 | no |
| Lambda Capture of »*this« | P0018R3 | c++17-lang | 7 | 3.9 | 19.1 |
| Using attribute Namespaces without Repetition | P0028R4 | c++17-lang | 7 | 3.9 | 19.1 |
| Dynamic Memory Allocation for over-aligned Data | P0035R4 | c++17-lang | 7 | 4 | no |
| Unary fold Expressions and empty Parameter Packs | P0036R0 | c++17-lang | 6 | 3.9 | no |
| »__has_include« in Preprocessor Conditionals | P0061R1 | c++17-lang | 5 | yes | 19.1 |
Wem die Informationen zu den großen Drei nicht genügen oder wer sich auch dafür interessiert, welche C++-Compiler die weiteren Unix-Plattformen mitbringen, der findet ebenfalls unter [5] weitergehende Informationen zu den aktuellen C++-Standards.
Der Support für die C++-Standardbibliothek (STL) variiert ein wenig. So gibt es zum jetzigen Zeitpunkt noch keine Implementierung der parallelen STL in C++17. Hier müssen sich Entwickler mit ein wenig Bastelarbeit Abhilfe schaffen: Die HPX-Bibliothek (High Performance Parallex, [6]) ist ein Framework für parallele und verteilte Applikationen und implementiert die parallele STL bereits.
Wichtige Compiler-Flags
Um den richtigen C++-Standard einzusetzen, muss der Entwickler ihn für den GCC- oder Clang-Compiler spezifizieren. Beide unterstützen die gleichen Flags. So übersetzt der Aufruf »g++ -std=c++11 dataRace.cpp« die Quellcodedatei »dataRace.cpp« gemäß dem C++11-Standard. Wer alternativ lieber C++14 oder C++17 verwenden möchte, gibt diese an. Das ist bei neueren Versionen von GCC und Clang in der Regel zwar nicht mehr nötig, doch kommen noch nicht alle Compiler-C++-Standard-Kombinationen ohne diese Angabe zurecht. Sie schadet daher nicht. MSVC benötigt indes keine Spezifikation des C++-Standards.
Es gibt für Entwickler noch einige weitere interessante Compiler-Flags. So erzeugen »-O3« (bei GCC und Clang) sowie »/Ox« (bei MSVC) zum Beispiel ein maximal optimiertes Programm, während »-Wall« (bei GCC und Clang) beziehungsweise (bei MSVC) »/Wall« den maximalen Warnlevel setzen.
| C++17 Features | Paper | Version | GCC | Clang | MSVC |
|---|---|---|---|---|---|
| Template Argument Deduction for Class Templates | P0091R3 | c++17-lang | 7 | 5 | no |
| Non-type Template Parameters with Auto Type | P0127R2 | c++17-lang | 7 | 4 | no |
| Guaranteed Copy Elision | P0135R1 | c++17-lang | 7 | 4 | no |
| New Specification for inheriting Constructors (DR1941 et al) | P0136R1 | c++17-lang | 7 | 3.9 | no |
| Direct-list-initialization of Enumerations | P0138R2 | c++17-lang | 7 | 3.9 | 19.1 |
| Stricter Expression Evaluation Order | P0145R3 | c++17-lang | 7 | 4 | no |
| »constexpr« Lambda Expressions | P0170R1 | c++17-lang | 7 | 5 | 19.1 |
| Differing Begin and End Types in range-based For | P0184R0 | c++17-lang | 6 | 3.9 | 19.1 |
| »[[fallthrough]]« Attribute | P0188R1 | c++17-lang | 7 | 3.9 | 19.1 |
| »[[nodiscard]]« Attribute | P0189R1 | c++17-lang | 7 | 3.9 | 19.1 |
| Pack Expansions in using-declarations | P0195R2 | c++17-lang | 7 | 4 | no |
| »[[maybe_unused]]« Attribute | P0212R1 | c++17-lang | 7 | 3.9 | 19.1 |
| Structured Bindings | P0217R3 | c++17-lang | 7 | 4 | 19.1 |
| Hexadecimal Floating-point Literals | P0245R1 | c++17-lang | 3 | yes | no |
| Ignore unknown Attributes | P0283R2 | c++17-lang | yes | 3.9 | no |
| »constexpr if« Statements | P0292R2 | c++17-lang | 7 | 3.9 | 19.1 |
| Init-statements for »if« and »switch« | P0305R1 | c++17-lang | 7 | 3.9 | 19.1 |
| Inline Variables | P0386R2 | c++17-lang | 7 | 3.9* | no |
| DR: Matching of Template Template-arguments excludes compatible Templates | P0522R0 | c++17-lang | 7 | 4 | no |
| Standardization of Parallelism TS | P0024R2 | c++17 | no | ||
| »std::uncaught_exceptions()« | N4259 | c++17 | 6 | 3.7 | 19.0 |
| Splicing Maps and Sets | P0083R3 | c++17 | 7 | no | |
| Improving »std::pair« and »std::tuple« | N4387 | c++17 | yes | 4 | 19.0 |
| »std::shared_mutex« (untimed) | N4508 | c++17 | 6 | 3.7 | 19.0 |
| Elementary String Conversions | P0067R5 | c++17 | no | ||
| »std::string_view« | N3921 | c++17 | 7 | 4 | 19.1 |
Sehr interessant ist auch der Sanitizer [7], der seit GCC 4.8 und Clang 3.2 zur Verfügung steht. MSVC-Benutzer müssen allerdings mit einer Windows-Portierung [8] ihr Glück versuchen. Der Sanitizer prüft, ob der vorliegende Code die Adressen, Threads und den Speicher des Programms fachgerecht nutzt.
Ein kleines Beispiel soll die Mächtigkeit des Thread Sanitizer unterstreichen. Das Programm in Listing 1 enthält Code, der recht offensichtlich für ein Data Race sorgen kann, da mehrere Threads zeitgleich auf die Variable »globalVal« zugreifen (siehe auch den C++-Artikel in der Programmieren-Rubrik). Genauer betrachtet versuchen beide Threads gleichzeitig, die Variable zu ändern.
Listing 1
Ein einfaches Data Race
01 #include <thread>
02
03 int main(){
04
05 int globalVar{};
06
07 std::thread t1([&globalVar]{ ++globalVar; });
08 std::thread t2([&globalVar]{ ++globalVar; });
09
10 t1.join();
11 t2.join();
12
13 }
In diesem Fall hilft der Thread Sanitizer [9], wenn ihn der Entwickler in GCC und Clang einbindet, indem er das Programm mit dem »-fsanitize=thread«-Flag übersetzt:
g++ -std=c++11 dataRace.cpp -fsanitize=thread -pthread -g -o dataRace
Führt der Entwickler das hier mit GCC übersetzte Programm anschließend aus, kommt es zu Ausgaben wie in Abbildung 2. Sie zeigt am Ende auch die Folge des eingebauten Data Race.
Rettungsnetz
Was aber tun, wenn der eigene Compiler den gewünschten C++-Standard noch nicht unterstützt? Hier kommt das Netz zur Hilfe. Es gibt eine große Fülle von Online-Compilern für C++, die im Hintergrund einen der großen drei Compiler verwenden. Arne Mertz pflegt eine exzellente Übersicht [10], der Artikel stellt weiter unten eine Auswahl von Compilern vor.
Für GCC- und Clang-Compiler testet der Artikel stellvertretend Wandbox [11], der eine beeindruckende Vielzahl von GCC- und Clang-Versionen unterstützt. Für Windows kommt der Visual C++ Compiler Online [12] zum Einsatz. Beeindruckender ist aber der Compiler Explorer [13] von Matt Godbolt. Er erzeugt keine ausführbaren Dateien, um sie anschließend zu starten, sondern generiert lediglich Assembler-Befehle für eine Vielzahl von Compilern.
Ein Alleinstellungsmerkmal zeigt der Online-C++-Compiler Coliru [14]: Er lässt sich in beliebige Webseiten integrieren. So verwendet ihn etwa die Online-C++-Referenz unter [15].
Eine Frage drängt sich vor einem genaueren Blick auf die Online-Compiler allerdings auf: Welchen Mehrwert bieten sie überhaupt? Darauf gibt es gleich mehrere Antworten:
- Sie erlauben es, neue Features des C++-Standards auszutesten. Damit lässt sich zum Beispiel herausfinden, ob ein Compiler-Upgrade überhaupt sinnvoll ist.
- Schwer verständliche Fehlermeldungen bekommen plötzlich Sinn, wenn ein anderer Compiler den Code übersetzt. Hier sticht vor allem Clang als Backend hervor.
- Code lässt sich leichter verifizieren. Undefiniertes Verhalten im Sourcecode hat verschiedene Effekte. Das Programm liefert das falsche oder vermeintlich richtige Ergebnis. Undefiniertes Verhalten bewirkt aber auch, dass sich Quellcode nicht übersetzen lässt oder das Programm zur Laufzeit abstürzt.
Wer seinen Code also online mit verschiedenen Compilern und Compilerversionen testet, verifiziert dessen Korrektheit einfach, aber effektiv.
Ein Beispiel lässt sich an Listing 2 demonstrieren. Dieser Code führt zu undefiniertem Verhalten. Seit C++11 unterstützt C++ die Delegation von Konstruktoren. Diese sollte keine Rekursion erzeugen, tut das im Beispiel aber trotzdem. Der Konstruktor für »char« ruft den für »double« auf, der wiederum den für »char« … und so weiter.
Listing 2
Rekursives Aufrufen von Konstruktoren
01 struct C {
02 C(char) : C(42.0) {} // ill-formed due to recursion
03 C(double) : C('a') {} // ill-formed due to recursion
04 };
05
06 int main(){
07 C('a');
08 C(3.5);
09 }
Wie gehen nun das Wandbox-Frontend (Clang, GCC) und der Visual C++ Online Compiler (MSVC) mit der Rekursion um? Während der GCC mit einem Segmentation Fault (Abbildung 3) zur Laufzeit aussteigt, begibt sich das Executable des MSVC in eine Endlosschleife, die das Compiler-Frontend abrupt unterbricht. Richtig vertrauenswürdig ist nur Clang (Abbildung 4). Er bemerkt bereits beim Übersetzen des Programms, dass eine Rekursion vorliegt.
Zum Abschluss übernimmt der schon erwähnte Compiler Explorer den Job. Er erzeugt für eine beeindruckende Anzahl von Compilern und Compilerversionen die Assembler-Instruktionen, unterstützt unter anderem GCC, Clang und MSVC. Diese Instruktionen stellt das grafische Frontend dabei Seite an Seite mit dem Sourcecode dar. Um die beiden einfacher zuzuordnen, hinterlegt der Compiler Explorer die Sourcecode-Zeilen und die korrespondierenden Assembler-Instruktionen farblich. Er verstärkt den Effekt noch, indem die Position des Mauszeigers im Sourcecode bewirkt, dass der Explorer die entsprechenden Assembler-Instruktionen hervorhebt. Abbildung 5 liefert einen ersten Eindruck.

Abbildung 5: Der Compiler Explorer hebt Sourcecode und zugehörige Assembler-Instruktionen farblich hervor.
Auch hier stellt sich wieder die Frage, welcher Nutzwert dem Compiler Explorer zukommt. Die Antwort umfasst gleich mehrere Aspekte.
- Indem er den Sourcecode den entsprechenden Assembler-Instruktionen gegenüberstellt, erhält der Anwender einen tieferen Einblick in den Übersetzungs- und Optimierungsprozess.
- Zugleich zeigt sich, ob der Compiler einen Ausdruck bereits zur Übersetzungszeit berechnet oder eine Funktion inline aufruft.
- Nicht zuletzt wird klar, wie die verschiedenen Optimierungslevel auf die Assembler-Instruktionen wirken.
Der letzte Punkt unterstreicht den großen Mehrwert des Compiler Explorers. Dank ihm ist es mit ein wenig Übung möglich, das oft nicht ganz intuitive Verhalten des Optimierers zu verstehen. Genau dazu dient das Beispiel in Listing 3.
Listing 3
Singleton Pattern
01 #include <chrono>
02 #include <iostream>
03
04 constexpr auto tenMill = 10000000;
05
06 class MySingleton{
07 public:
08 static MySingleton& getInstance(){
09 static MySingleton instance;
10 return instance;
11 }
12 private:
13 MySingleton() = default;
14 ~MySingleton() = default;
15 MySingleton(const MySingleton&) = delete;
16 MySingleton& operator=(const MySingleton&) = delete;
17
18 };
19
20 int main(){
21
22 auto begin = std::chrono::steady_clock:: now();
23 for (size_t i = 0; i <= tenMill; ++i){
24 MySingleton::getInstance();
25 }
26 std::chrono::duration<double> res= std::chrono::steady_clock::now() - begin;
27
28 std::cout << res.count() << std::endl;
29
30 }
Darin kommt das Singleton Pattern zum Einsatz (Zeilen 6 bis 18). Die große Frage ist, wie viel Zeit nötig ist, das Singleton zehn Millionen Mal aufzurufen (Zeile 24). Genau diese Antwort gibt Zeile 28. Mit dem GCC ist das Programm schnell ohne und mit maximaler Optimierung (»-O3«) übersetzt. Abbildung 6 liefert Zahlen in Sachen Performance.
Überrascht? Dazu gibt es allen Grund, denn die maximal optimierte Ausführung geschieht viel zu schnell. Wenn uns da nicht gerade der Optimierer einen Streich gespielt hat. Der Blick auf den Compiler Explorer bestätigt es. Abbildung 7 stellt den Sourcecode in der nicht-optimierten Variante dar, Abbildung 8 zeigt die entsprechenden Assembler-Instruktionen. Das schaut unverdächtig aus.
Deutlich verdächtiger ist da schon die Abbildung 9. Der Singleton-Aufruf »MySingleton::getInstance();« ist nicht farblich hinterlegt. Das kann nur bedeuten, dass es dazu keine passenden Assembler-Iinstruktionen gibt. Genau das zeigt auch Abbildung 10: Hier fehlen nicht nur die Instruktionen für den Singleton-Aufruf, sondern auch für die For-Schleife. Wie ist das möglich? Da das Ausführen der For-Schleife keinen Effekt hat, darf der Optimierer sie entfernen. Das tut er auch – was aber die Messung der Performance ad absurdum führt.
Kenne deine Compiler!
Besonders die Online-Compiler bieten eine unschlagbare Hilfe, wenn es darum geht, mit dem Drei-Jahres-Takt der modernen C++-Standards Schritt zu halten. So stellt sich für zeitgemäße C++-Entwickler gar nicht unbedingt die Frage, ob sie den besten aller C++-Compiler verwenden.
Am Ende zeigt jeder Compiler Stärken und Schwächen. So setzen der GCC und Clang die C++-Kernsprache meist schneller um als der MSVC. Der hat aber beim Umsetzen der C++-Bibliothek oft die Nase vorn. Es stellt sich also die Frage: Wie lässt sich die Funktionsvielfalt der großen drei optimal nutzen? Die Antwort ist einfach: Durch die hervorragenden Online-Compiler.
Infos
- GCC: https://gcc.gnu.org
- Clang: https://clang.llvm.org
- Microsoft Visual C++ (MSVC): https://docs.microsoft.com/en-us/cpp/
- GNU ARM Toolchain: https://developer.arm.com/open-source/gnu-toolchain/gnu-rm
- Der C++17-Standard, seine Features und der Compiler-Support im Überblick: http://en.cppreference.com/w/cpp/compiler_support
- Eine parallele STL implementiert HPX (High Performance Parallex): http://stellar.cct.lsu.edu/projects/hpx/
- Der Sanitizer prüft den Einsatz von Adressen, Speicher und Threads: https://github.com/google/sanitizers/wiki
- Windows-Portierung des Sanitizer: https://github.com/google/sanitizers/wiki/AddressSanitizerWindowsPort
- Thread Sanitizer: https://github.com/google/sanitizers/wiki/ThreadSanitizerCppManual
- Arne Mertz’ Sammlung von C++-Online-Compilern: https://arnemertz.github.io/online-compilers/
- Online-Compiler Wandbox: https://wandbox.org
- Online-Version des Visual C++ Compiler: http://webcompiler.cloudapp.net
- Compiler Explorer: https://godbolt.org
- Coliru: http://coliru.stacked-crooked.com
- C++-Referenz Online: http://en.cppreference.com/w/














