Ich habe kürzlich über Move-Konstruktoren in C ++ gelesen (siehe z. B. hier ) und ich versuche zu verstehen, wie sie funktionieren und wann ich sie verwenden sollte.
Soweit ich weiß, wird ein Verschiebungskonstruktor verwendet, um die Leistungsprobleme zu verringern, die durch das Kopieren großer Objekte verursacht werden. Auf der Wikipedia-Seite heißt es: "Ein chronisches Leistungsproblem mit C ++ 03 sind die kostspieligen und unnötigen tiefen Kopien, die implizit auftreten können, wenn Objekte als Wert übergeben werden."
Normalerweise spreche ich solche Situationen an
- durch Weitergabe der Objekte als Referenz oder
- durch die Verwendung von intelligenten Zeigern (z. B. boost :: shared_ptr), um das Objekt zu umgehen (die intelligenten Zeiger werden anstelle des Objekts kopiert).
In welchen Situationen sind die beiden oben genannten Techniken nicht ausreichend und die Verwendung eines Verschiebungskonstruktors ist praktischer?
c++
programming-practices
Giorgio
quelle
quelle
shared_ptr
nur um schnelles Kopieren zu ermöglichen) und wenn die Verschiebungssemantik das Gleiche ohne Beeinträchtigung von Codierung, Semantik und Sauberkeit erreichen kann.Antworten:
Mit der Verschiebungssemantik wird C ++ um eine ganze Dimension erweitert. Sie können damit nicht nur günstig Werte zurückgeben.
Zum Beispiel, ohne Bewegung-Semantik
std::unique_ptr
funktioniert nicht - schauen Siestd::auto_ptr
, was mit der Einführung der Bewegung-Semantik veraltet und in C ++ 17 entfernt wurde. Das Verschieben einer Ressource unterscheidet sich erheblich vom Kopieren. Es ermöglicht die Übertragung des Eigentums an einem einzigartigen Gegenstand.Schauen wir uns das zum Beispiel nicht an
std::unique_ptr
, da es ziemlich gut diskutiert wird. Sehen wir uns beispielsweise ein Vertex Buffer-Objekt in OpenGL an. Ein Vertex-Puffer repräsentiert den Speicher auf der GPU. Er muss mithilfe spezieller Funktionen zugewiesen und freigegeben werden, wobei möglicherweise die Lebensdauer stark eingeschränkt ist. Es ist auch wichtig, dass nur ein Besitzer es benutzt.Dies könnte nun mit a geschehen,
std::shared_ptr
aber diese Ressource darf nicht gemeinsam genutzt werden. Dies macht es verwirrend, einen gemeinsam genutzten Zeiger zu verwenden. Sie könnten verwendenstd::unique_ptr
, aber das erfordert immer noch eine Verschiebungssemantik.Natürlich habe ich keinen Verschiebungskonstruktor implementiert, aber Sie haben die Idee.
Das Relevante dabei ist, dass einige Ressourcen nicht kopierbar sind . Sie können Zeiger weitergeben, anstatt sie zu verschieben. Wenn Sie jedoch unique_ptr verwenden, liegt das Problem des Eigentums vor. Es lohnt sich, so klar wie möglich zu sein, was die Absicht des Codes ist, daher ist ein Move-Konstruktor wahrscheinlich der beste Ansatz.
quelle
Die Bewegungssemantik ist nicht unbedingt eine große Verbesserung, wenn Sie einen Wert zurückgeben - und wenn Sie einen
shared_ptr
(oder einen ähnlichen) verwenden, pessimieren Sie wahrscheinlich vorzeitig. In Wirklichkeit machen fast alle einigermaßen modernen Compiler das, was als Return Value Optimization (RVO) und Named Return Value Optimization (NRVO) bezeichnet wird. Dies bedeutet , dass wenn Sie einen Wert sind Rückkehr, statt Kopieren tatsächlich den Wert überhauptSie übergeben einfach einen versteckten Zeiger / Verweis darauf, wo der Wert nach der Rückgabe zugewiesen wird, und die Funktion verwendet diesen, um den Wert dort zu erstellen, wo er enden wird. Der C ++ - Standard enthält spezielle Vorkehrungen, um dies zu ermöglichen. Selbst wenn (zum Beispiel) Ihr Kopierkonstruktor sichtbare Nebenwirkungen hat, ist es nicht erforderlich, den Kopierkonstruktor zu verwenden, um den Wert zurückzugeben. Beispielsweise:Die Grundidee hier ist ziemlich einfach: Erstellen Sie eine Klasse mit genügend Inhalten, die Sie nach Möglichkeit lieber nicht kopieren möchten
std::vector
möchten wir mit 32767 zufälligen Ints füllen). Wir haben eine explizite Kopie, die uns anzeigt, wann / ob sie kopiert wird. Wir haben auch etwas mehr Code, um etwas mit den Zufallswerten im Objekt zu tun, so dass das Optimierungsprogramm (zumindest leicht) nicht alles an der Klasse eliminiert, nur weil es nichts tut.Wir haben dann Code, um eines dieser Objekte von einer Funktion zurückzugeben, und verwenden dann die Summierung, um sicherzustellen, dass das Objekt wirklich erstellt und nicht nur vollständig ignoriert wird. Wenn wir es zumindest mit den neuesten / modernsten Compilern ausführen, stellen wir fest, dass der von uns geschriebene Kopierkonstruktor überhaupt nicht ausgeführt wird - und ja, ich bin mir ziemlich sicher, dass sogar eine schnelle Kopie mit einem
shared_ptr
noch langsamer ist als das Kopieren überhaupt.Durch Umzug können Sie eine ganze Reihe von Dingen erledigen, die Sie (direkt) ohne sie nicht erledigen könnten. Betrachten Sie den Zusammenführungsteil einer externen Zusammenführungssorte - Sie haben beispielsweise 8 Dateien, die Sie zusammenführen möchten. Idealerweise möchten Sie alle 8 dieser Dateien in ein
vector
- einfügen, aber davector
(ab C ++ 03) Elementeifstream
kopiert werden müssen und s nicht kopiert werden können, stecken Sie mit einigenunique_ptr
/ festshared_ptr
. oder etwas in dieser Reihenfolge, um sie in einen Vektor setzen zu können. Beachten Sie, dass der Compiler das auch dann nicht weiß, wenn wir (zum Beispiel) einreserve
Leerzeichen in setzen,vector
so dass wir sicher sind, dass unserifstream
s nie wirklich kopiert wird, so dass der Code nicht kompiliert wird, obwohl wir wissen, dass der Copy-Konstruktor es niemals sein wird trotzdem benutzt.Auch wenn es immer noch nicht kopiert werden, in C ++ 11 ein
ifstream
kann bewegt werden. In diesem Fall wendet sich die wahrscheinlich nicht wird immer bewegt werden, aber die Tatsache , dass sie hält notwendig sein könnte , wenn der Compiler glücklich, so dass wir unsere setzen könnenifstream
Objekte in einvector
direkt, ohne Smart - Pointer - Hacks.Ein Vektor, der es tut expandiert, ist ein ziemlich gutes Beispiel für eine Zeit, in der Bewegungssemantik wirklich nützlich sein kann / ist. In diesem Fall hilft RVO / NRVO nicht, da es sich nicht um den Rückgabewert einer Funktion (oder etwas sehr Ähnliches) handelt. Wir haben einen Vektor, der einige Objekte enthält, und wir möchten diese Objekte in einen neuen, größeren Speicherbereich verschieben.
In C ++ 03 wurden dazu Kopien der Objekte im neuen Speicher erstellt und anschließend die alten Objekte im alten Speicher gelöscht. Es war jedoch Zeitverschwendung, all diese Kopien nur zum Wegwerfen der alten Kopien anzufertigen. In C ++ 11 können Sie erwarten, dass sie stattdessen verschoben werden. Auf diese Weise können wir im Wesentlichen eine flache Kopie anstelle einer (im Allgemeinen viel langsameren) tiefen Kopie erstellen. Mit anderen Worten, mit einer Zeichenfolge oder einem Vektor (für nur einige Beispiele) kopieren wir nur die Zeiger in die Objekte, anstatt Kopien aller Daten zu erstellen, auf die sich diese Zeiger beziehen.
quelle
Erwägen:
Wenn Sie Zeichenfolgen zu v hinzufügen, wird diese nach Bedarf erweitert, und bei jeder Neuzuweisung müssen die Zeichenfolgen kopiert werden. Bei Move-Konstruktoren ist dies im Grunde kein Problem.
Natürlich können Sie auch Folgendes tun:
Das funktioniert aber nur gut, weil der
std::unique_ptr
move-Konstruktor implementiert ist.Die Verwendung
std::shared_ptr
ist nur in (seltenen) Situationen sinnvoll, in denen Sie tatsächlich das gemeinsame Eigentum haben.quelle
string
eine InstanzFoo
mit 30 Datenelementen vorhanden ist? Dieunique_ptr
Version wäre nicht effizienter?Rückgabewerte sind die Stellen, an denen ich am häufigsten den Wert anstelle einer Referenz übergeben möchte. In der Lage zu sein, ein Objekt schnell "auf dem Stapel" zurückzugeben, ohne einen massiven Leistungsnachteil zu erleiden, wäre schön. Auf der anderen Seite ist es nicht besonders schwierig, dies zu umgehen (gemeinsame Zeiger sind einfach zu bedienen ...), daher bin ich mir nicht sicher, ob es sich wirklich lohnt, zusätzliche Arbeit an meinen Objekten zu leisten, nur um dies tun zu können.
quelle