Die mit C++11 eingeführte Move-Semantik ermöglicht es, Ressourcen wie dynamischen Speicher effizienter von einem Objekt auf ein anderes zu übertragen. Sie vermeidet die Kosten tiefer Objektkopien – insbesondere bei großen Objekten –, indem ein Objekt die Ressourcen eines anderen Objekts nach Möglichkeit „übernehmen“ kann, statt sie zu duplizieren.
Vor C++11 wurde beim Zuweisen eines Objekts an ein anderes ein Kopierkonstruktor oder Zuweisungsoperator aufgerufen, was zu einer tiefen Kopie der Objektdaten führte. Dieser Vorgang konnte besonders bei großen Objekten oder Objekten mit dynamischen Speicherallokationen teuer sein. Move-Semantik ist daher besonders nützlich, wenn Performance entscheidend ist, etwa im High-Performance Computing, in der Spieleentwicklung oder in ressourcenbeschränkten Umgebungen.
Die Move-Semantik führt Rvalue-Referenzen sowie Move-Konstruktoren und Move-Zuweisungsoperatoren ein.
So funktioniert es:
- Rvalue-Referenzen (
&&): Rvalue-Referenzen ermöglichen die Bindung an temporäre Objekte (Rvalues), also häufig an Objekte, die kurz vor ihrer Zerstörung stehen oder nicht mehr benötigt werden. Sie werden durch ein doppeltes kaufmännisches Und gekennzeichnet&&. - Move-Konstruktoren: Ein Move-Konstruktor ist ein spezieller Konstruktor, der eine Rvalue-Referenz auf ein anderes Objekt desselben Typs entgegennimmt und dessen Ressourcen in das neue Objekt „verschiebt“. Dazu übernimmt er die internen Zeiger oder Handles des Quellobjekts und setzt diese dort zurück. Das Quellobjekt bleibt dadurch in einem gültigen, aber nicht näher spezifizierten Zustand. Dies geschieht typischerweise mithilfe von
std::move()-Funktion verwendet, die einen Lvalue in einen Rvalue umwandelt. - Move-Zuweisungsoperatoren: Ähnlich wie Move-Konstruktoren ermöglichen Move-Zuweisungsoperatoren die effiziente Übertragung von Ressourcen zwischen Objekten. Sie nehmen eine Rvalue-Referenz auf ein anderes Objekt desselben Typs entgegen und führen eine Move-Zuweisung aus, wobei das Quellobjekt in einem gültigen Zustand verbleibt.
Muss man diese Funktion jedoch vollständig beherrschen, um von ihren erheblichen Performance-Vorteilen zu profitieren? Zum Glück erledigt der Compiler in vielen Fällen hervorragende Arbeit und nutzt Move-Semantik automatisch, wann immer dies möglich ist.
Betrachten wir dieses einfache Beispiel:
class test
{
};
test f()
{
test t;
return t;
}
In diesem Beispiel verwendet der Entwickler die Move-Funktion nicht ausdrücklich; der Compiler setzt sie jedoch implizit ein. Wenn eine Funktion ein Objekt als Wert zurückgibt, wurde traditionell eine Kopie des Objekts erzeugt: Speicher wurde für die Kopie reserviert und anschließend der Inhalt des ursprünglichen Objekts dorthin kopiert. Mit Move-Semantik können stattdessen – sofern möglich – die Ressourcen des lokalen Objekts innerhalb der Funktion auf das zurückgegebene Objekt übertragen werden.
Wie lässt sich erkennen, wo der Compiler diese Funktion automatisch verwendet?
1 – Mit dem Befehl clang -cc1 -ast-dump
Der AST-Dump-Befehl (Abstract Syntax Tree) von Clang ist ein leistungsfähiges Werkzeug des Clang-Compiler-Frontends für C, C++ und Objective-C. Entwickler können damit die interne Darstellung ihres Quellcodes als abstrakten Syntaxbaum untersuchen – eine hierarchische Darstellung der Quellcodestruktur.
Führen wir diesen Befehl für unser einfaches Beispiel aus.
|-CXXRecordDecl 0x1ccdf79b618 <test.cpp:1:1, line:4:1> line:1:7 referenced class test definition
| |-DefinitionData pass_in_registers empty aggregate standard_layout trivially_copyable pod trivial literal has_constexpr_non_copy_move_ctor can_const_default_init
| | |-DefaultConstructor exists trivial constexpr defaulted_is_constexpr
| | |-CopyConstructor simple trivial has_const_param implicit_has_const_param
| | |-MoveConstructor exists simple trivial
| | |-CopyAssignment simple trivial has_const_param needs_implicit implicit_has_const_param
| | |-MoveAssignment exists simple trivial needs_implicit
| | `-Destructor simple irrelevant trivial needs_implicit
| |-CXXRecordDecl 0x1ccdf79b738 <col:1, col:7> col:7 implicit class test
| |-CXXConstructorDecl 0x1ccdf888b58 <col:7> col:7 implicit used constexpr test 'void () noexcept' inline default trivial
| | `-CompoundStmt 0x1ccdf889098 <col:7>
| |-CXXConstructorDecl 0x1ccdf888cc8 <col:7> col:7 implicit constexpr test 'void (const test &)' inline default trivial noexcept-unevaluated 0x1ccdf888cc8
| | `-ParmVarDecl 0x1ccdf888de8 <col:7> col:7 'const test &'
| `-CXXConstructorDecl 0x1ccdf888ec8 <col:7> col:7 implicit used constexpr test 'void (test &&) noexcept' inline default trivial
| |-ParmVarDecl 0x1ccdf888fe8 <col:7> col:7 'test &&'
| `-CompoundStmt 0x1ccdf8892a0 <col:7>
`-FunctionDecl 0x1ccdf8889d8 <line:5:1, line:9:1> line:5:6 f 'test ()'
`-CompoundStmt 0x1ccdf8892f8 <line:6:1, line:9:1>
|-DeclStmt 0x1ccdf8891f0 <line:7:5, col:11>
| `-VarDecl 0x1ccdf888ad8 <col:5, col:10> col:10 used t 'test':'test' nrvo callinit
| `-CXXConstructExpr 0x1ccdf8891c8 <col:10> 'test':'test' 'void () noexcept'
`-ReturnStmt 0x1ccdf8892e0 <line:8:5, col:12>
`-CXXConstructExpr 0x1ccdf8892b0 <col:12> 'test':'test' 'void (test &&) noexcept'
`-ImplicitCastExpr 0x1ccdf889228 <col:12> 'test':'test' xvalue <NoOp>
`-DeclRefExpr 0x1ccdf889208 <col:12> 'test':'test' lvalue Var 0x1ccdf888ad8 't' 'test':'test'
Wie zu sehen ist, erzeugt der Compiler den Move-Konstruktor und den Move-Zuweisungsoperator für die Testklasse.
| | |-MoveConstructor exists simple trivial
| | |-CopyAssignment simple trivial has_const_param needs_implicit implicit_has_const_param
| | |-MoveAssignment exists simple trivial needs_implicit
Für die return-Anweisung in der Funktion f() wird der Move-Konstruktor aufgerufen.
`-ReturnStmt 0x1ccdf8892e0 <line:8:5, col:12>
`-CXXConstructExpr 0x1ccdf8892b0 <col:12> 'test':'test' 'void (test &&) noexcept'
`-ImplicitCastExpr 0x1ccdf889228 <col:12> 'test':'test' xvalue <NoOp>
`-DeclRefExpr 0x1ccdf889208 <col:12> 'test':'test' lvalue Var 0x1ccdf888ad8 't' 'test':'test'
Dank des C++11-Standards nutzen moderne Compiler Move-Semantik in allen Fällen, in denen sie problemlos eingesetzt werden kann. Gegenüber C++-Compilern aus der Zeit vor C++11 ergibt sich dadurch ein Performance-Vorteil.
2 – Mit CppDepend
CppDepend verwendet intern Clang, um den Quellcode zu parsen und den AST zu durchlaufen. So werden möglichst viele nützliche Daten für die Berechnung von Metriken sowie für die Erkennung von Struktur und Abhängigkeiten Ihrer Codebasis gewonnen.
Für die Testklasse ist der erzeugte Move-Konstruktor im Klassenbrowser von CppDepend sichtbar:

Um zu sehen, wo er verwendet wird, klicken wir einfach mit der rechten Maustaste auf den Move-Konstruktor und wählen die Methoden aus, die ihn verwenden.

Das Ergebnis:

Wie zu sehen ist, verwendet die Funktion f() den Move-Konstruktor implizit.
CppDepend übernimmt den AST von Clang und wandelt ihn in ein geeignetes Modell um, das sich mit unserer Code-Abfragesprache durchsuchen lässt:

Wenn Sie wissen möchten, wo Move-Semantik in Ihren C++-Projekten verwendet wird, können Sie mit einem dieser beiden Ansätze mehr über Ihre Codebasis erfahren.
