Manchmal muss der C++-Compiler temporäre Objekte erzeugen. Sie werden in folgenden Situationen verwendet:
- Initialisierung von Referenzen.
- Auswertung von Ausdrücken einschließlich Standard-Typkonvertierungen.
- Übergabe von Argumenten.
- Rückgabe aus Funktionen.
- Auswertung eines throw-Ausdrucks.
Bei nicht trivialen Klassen kann das Erzeugen und Zerstören temporärer Objekte sowohl Rechenzeit als auch Speicher kosten. In solchen Fällen sollte ihre Entstehung möglichst vermieden werden. Der C++-Compiler eliminiert zwar einige temporäre Objekte, kann jedoch nicht alle beseitigen.
Es ist nicht immer einfach zu erkennen, an welchen Stellen temporäre Objekte entstehen, wie Herb Sutter in seinem Beispiel. Der Compiler verfügt über diese Information. Doch lässt sich vom Compiler ermitteln, an welchen Stellen temporäre Objekte erzeugt werden? Und falls nicht: Lässt er sich leicht so anpassen, dass er diese Stellen meldet?
In unserem Fall verwenden wir Clang. Der Compiler ist sehr flexibel und bietet zahlreiche Möglichkeiten, sein Verhalten anzupassen. Ein zentrales Designprinzip von Clang ist seine bibliotheksbasierte Architektur. Dadurch lassen sich verschiedene Teile des Frontends sauber in separate Bibliotheken aufteilen, die je nach Anforderung unterschiedlich kombiniert werden können. Dieser Ansatz fördert zudem klar definierte Schnittstellen und erleichtert neuen Entwicklern den Einstieg, da sie zunächst nur kleine Teile des Gesamtsystems verstehen müssen.
Der Clang-Compiler arbeitet in drei Phasen:
- Das Frontend analysiert den Quellcode, prüft ihn auf Fehler und erzeugt einen sprachspezifischen Abstract Syntax Tree (AST), der den Eingabecode repräsentiert.
- Der Optimierer führt Optimierungen am vom Frontend erzeugten AST durch.
- Das Backend erzeugt den endgültigen, von der Maschine auszuführenden Code und ist von der Zielplattform abhängig.

Wir konzentrieren uns auf die Frontend-Phase. Ziel ist es, den Abstract Syntax Tree (AST) einer Quelldatei zu erhalten und zu prüfen, ob er nützliche Informationen über temporäre Objekte liefert.
Sehen wir uns den AST dieses minimalen Quellcodes an:

Um den AST zu erzeugen, können wir den Clang-Frontend-Parser mit dem Schalter -cc1 ausführen.
clang -cc1 -ast-dump test.cpp
Hier ist der für die Funktion GetTest erzeugte AST:

In diesem AST beziehen sich zwei Informationen auf temporäre Objekte: nrvo and elidable.
Named Return Value Optimization (NRVO) ist eine Compiler-Optimierung, bei der das temporäre Objekt entfällt, das normalerweise den Rückgabewert einer Funktion aufnehmen würde. NRVO vermeidet den Kopierkonstruktor und Destruktor eines stackbasierten Rückgabewerts. Dadurch werden redundante Aufrufe von Kopierkonstruktor und Destruktor entfernt und die Gesamtleistung verbessert. Weitere Details finden Sie auf der Wiki-Seite.
Copy Elision ist eine Compiler-Optimierung, die unnötige Kopien von Objekten vermeidet. Weitere Details finden Sie auf der Wiki-Seite.
Interessant ist, dass Clang anzeigt, wo NRVO angewendet wird, aber nicht ausdrücklich meldet, wo temporäre Objekte entstehen. Sehen wir uns daher den Quellcode des AST-Dumpers an und versuchen wir, diese Stellen auszugeben.
Wie funktioniert ASTDumper?
Der Compiler analysiert ein Programm und stellt es als Abstract Syntax Tree (AST) dar. Der AST enthält viele unterschiedliche Knotentypen, etwa für Zuweisungen, Variablenreferenzen und arithmetische Ausdrücke. Nach der Erzeugung des AST führt Clang verschiedene Frontend-Aktionen aus, die den Baum durchlaufen und verarbeiten. ASTDumpAction ist eine davon und gibt den AST auf der Konsole aus.
ASTDumper wird wie folgt deklariert:
class ASTDumper
: public ConstDeclVisitor, public ConstStmtVisitor,
public ConstCommentVisitor
Das Visitor-Pattern eignet sich besonders, wenn eine Struktur durchlaufen und auf jeden ihrer Knoten eine bestimmte Verarbeitung angewendet werden soll.
Hier sind einige Methoden, die beim Durchlaufen des AST aufgerufen werden. Jede davon gehört zu einem bestimmten AST-Knotentyp.
void VisitNamespaceDecl(const NamespaceDecl *D);
void VisitUsingDirectiveDecl(const UsingDirectiveDecl *D);
void VisitNamespaceAliasDecl(const NamespaceAliasDecl *D);
void VisitTypeAliasDecl(const TypeAliasDecl *D);
void VisitTypeAliasTemplateDecl(const TypeAliasTemplateDecl *D);
void VisitCXXRecordDecl(const CXXRecordDecl *D);
void VisitStaticAssertDecl(const StaticAssertDecl *D);
Den Clang-AST-Dumper anpassen
Unser Ziel ist es, die erzeugten temporären Objekte auszugeben. Dazu müssen wir die Objekterzeugung verfolgen und feststellen, welcher AST-Knoten an der Konstruktion des jeweiligen Objekts beteiligt ist.
In Clang wird VisitCXXConstructExpr aufgerufen, wenn ein C++-Objekt erzeugt werden muss. Hier ist die Implementierung:

Wie wir sehen, wird nicht geprüft, ob das erzeugte Objekt temporär ist. Die gute Nachricht: CXXConstructExpr verfügt über die Methode IsTemporaryObject, mit der festgestellt werden kann, ob das Ergebnis dieses Ausdrucks ein temporäres Objekt des angegebenen Klassentyps ist.
Ändern wir die Implementierung und fügen eine weitere Bedingung hinzu:

Hier ist der nach der Änderung erzeugte neue AST:

Nach nur wenigen zusätzlichen Codezeilen meldet ASTDumper nun ausdrücklich, an welchen Stellen temporäre Objekte erzeugt werden.
Fazit
LLVM/Clang ist nicht nur ein Compiler, sondern auch eine leistungsfähige Infrastruktur für die Entwicklung eigener C/C++/Objective-C-Werkzeuge. Die Architektur ist vergleichsweise leicht zu verstehen und lässt sich an individuelle Anforderungen anpassen. Scheuen Sie sich nicht, den Clang-Quellcode herunterzuladen, einige Änderungen vorzunehmen und ihn neu zu kompilieren – besonders für Studierende, die verstehen möchten, wie Compiler funktionieren, kann das sehr lehrreich sein.
