Ist der pass-by-value-and-dann-bewegen konstruieren eine schlechte Redewendung?
Da haben wir die move-Semantik in C++, heute ist es üblich, zu tun,
void set_a(A a) { _a = std::move(a); }
Die Argumentation ist, dass, wenn a
ist ein rvalue, wird die Kopie erstellte, und es wird nur ein verschieben.
Aber was passiert, wenn a
ist ein lvalue? Es scheint, es wird eine Kopie Konstruktion und dann ein move-Zuweisung (unter der Annahme Einer hat einen richtigen move-Zuweisungs-operator). Verschieben von Aufgaben kann teuer werden, wenn das Objekt zu viele member-Variablen.
Auf der anderen Seite, wenn wir
void set_a(const A& a) { _a = a; }
Es wird nur eine Kopie der Abtretung. Können wir sagen, dieser Weg wird bevorzugt über den pass-by-value-idiom, wenn wir passieren lvalues?
- Aufruf
std::move
auf eineconst&
gibt eineconst&&
, die nicht verschoben werden können, aus. - Sie haben Recht, ich bearbeitete es.
- Auch relevant: stackoverflow.com/questions/15600499/....
- C++ - Kern-Richtlinien haben, die Regel F. 15 (fortgeschrittene) für diesen Fall isocpp.github.io/CppCoreGuidelines/...
- Bezogen ist dieser Vortrag von Nicolai Josuttis, die diskutiert einige Möglichkeiten: youtube.com/watch?v=PNRju6_yn3o
- Einige gute Lektüre: stackoverflow.com/questions/26261007/..., schauen Sie auch die genannten Folien und CppCon 2014 Vortrag von Herb Sutter.
Du musst angemeldet sein, um einen Kommentar abzugeben.
Teuer-to-move-Arten sind selten in der modernen C++ - Nutzung. Wenn Sie sind besorgt über die Kosten für den Umzug, schreiben Zugriffsbeschränkungen:
oder eine perfect-forwarding setter:
wird akzeptieren, dass lvalues, rvalues, und alles, was implizit konvertierbar
decltype(_a)
ohne extra kopiert oder verschiebt.Trotz eine zusätzliche verschieben, wenn die Einstellung von ein lvalue, die Sprache ist nicht bad da (a) die überwiegende Mehrheit der Arten bieten Konstante-die Zeit vergeht und (b) copy-and-swap liefert exception-Sicherheit und Nähe-die optimale Leistung in einer einzigen Zeile code.
template <typename T> auto set_a(T&& a) -> decltype((void)(_a = std::forward<T>(a))) { _a = std::forward<T>(a); }
. Eingeschränkte oder nicht, Fehlermeldungen tendieren dazu, zu verwirren.T
ist etwas, das konstruiert werden könnte, durch einestd::initializer_list
diese können Sie nicht verwenden Sie eine Liste, in der Aufforderung.set_a({1,2,3})
würde hae zuset_a(A{1,2,3})
seit verspannt-init-Liste's haben keinen Typ.void set_a(const A& a) { _a = std::move(a); }
;void set_a(A&& a) { _a = std::move(a); };
? Ich fügestd::move(a)
in der ersten Anweisung.Problem gut erkannt. Ich würde nicht so weit gehen, zu sagen, dass die pass-by-value-and-dann-bewegen konstruieren ist ein schlechter idiom, aber es hat definitiv seine Tücken lauern.
Wenn das Ihre Art ist teuer, zu verschieben und /oder verschieben ist im Grunde nur eine Kopie, dann die pass-by-value-Ansatz ist suboptimal. Beispiele für solche Arten umfassen würde, die Typen mit fester Größe array als member: kann Es relativ teuer, sich zu bewegen und eine Bewegung ist nur eine Kopie. Siehe auch
in diesem Zusammenhang.
Den pass-by-value-Ansatz hat den Vorteil, dass Sie nur halten müssen, um eine Funktion, sondern Sie zahlen dafür mit Leistung. Es kommt auf deine Anwendung an, ob diese Pflege Vorteil überwiegt den Verlust in der Leistung.
Der pass von lvalue und rvalue reference-Ansatz kann dazu führen, Wartung Kopfschmerzen schnell, wenn Sie mehrere Argumente. Bedenken Sie:
Wenn Sie mehrere Argumente, haben Sie eine permutation problem. In diesem sehr einfachen Beispiel, es ist wohl doch nicht so schlecht zu halten, die diese 4 Konstruktoren. Aber schon in diesem einfachen Fall würde ich ernsthaft erwägen, mit dem pass-by-value-Ansatz mit einer einzigen Funktion
C(A a, B b) : a(move(a)), b(move(b)) { }
anstelle der oben genannten 4 Konstruktoren.
Also lange Geschichte kurz, weder Ansatz ist ohne Nachteile. Treffen Sie Ihre Entscheidungen auf der Grundlage der tatsächlichen profiling-Informationen, anstatt die Optimierung vorzeitig.
std::
)arrays
die mehr sind relativ teuer, sich zu bewegen, als zu kopieren, wie Sie erwähnen? Anstatt also mit pass-by-value + move zuweisen, sollte man pass-by-value, ohne verschieben, zuweisen oder übergabe per const-ref als Faustregel können (z.B. einfache setter)?float
/double
sowohl in performance-kritischen und nicht-kritischen game-engine-code.Für den Allgemeinen Fall , wo der Wert gespeichert, die pass-by-value nur ein guter Kompromiss-
Für den Fall, wo Sie wissen, dass nur lvalues weitergegeben werden (einige eng gekoppelten code) es ist unsinnig, unsmart.
Für den Fall, wo man vermutet eine Verbesserung der Geschwindigkeit, indem beide, zuerst DENKEN, ZWEIMAL, und wenn das nicht hilft, MESSEN.
Denen der Wert wird nicht gespeichert werden, bevorzuge ich die übergabe per Referenz, denn das verhindert, dass zig unnötige Kopiervorgänge.
Schließlich, wenn die Programmierung reduziert werden könnte, um gedankenlose Anwendung von Regeln, könnten wir es verlassen, um Roboter. Also IMHO ist es nicht eine gute Idee, um den Fokus so sehr auf die Regeln. Besser auf das konzentrieren, was die Vorteile und die Kosten, für verschiedene Situationen. Die Kosten umfassen nicht nur die Geschwindigkeit, sondern z.B. auch code-Größe und Klarheit. Regeln können im Allgemeinen nicht mit solchen Interessenkonflikten.
Aktuelle Antworten sind Recht unvollständig. Stattdessen werde ich versuchen zu dem Schluss, basierend auf den Listen der vor-und Nachteile finde ich.
Kurze Antwort
Kurz gesagt, es kann OK sein, manchmal aber auch schlecht.
Dieses idiom, nämlich Vereinheitlichung - Schnittstelle, hat eine bessere Klarheit (sowohl in der Konzeption und Umsetzung) im Vergleich zur Weiterleitung von Vorlagen oder anderen überlastungen. Es wird manchmal mit copy-and-swap (tatsächlich, sowie verschieben und swap - in diesem Fall).
Detaillierte Analyse
Die Profis sind:
const
-qualifizierten).const
, abervolatile
, die noch weiter verringern gewöhnliche überlastungen.const
,const
,const volatile
} Kombinationen für n Parameter.T
kann es immer noch clash mit überladungen mit einem parameterconst T&
in der gleichen position, denn das argument kann ein lvalue des TypsT
und die Schablone instantiiert mit Typ -T&
(stattconst T&
) denn es ist mehr bevorzugt, die durch die überlastung Regel, wenn es keine andere Möglichkeit gibt zu unterscheiden, welche ist die beste überlastung Kandidaten. Diese Inkonsistenz kann ganz überraschend.P&&
eine KlasseC
. Wie viele Zeit werden Sie vergessen, ausgeschlossen die Instanz vonP&&
Weg von möglicherweise cv-qualifiziertC
von SFINAE (z.B. durch hinzufügentypename = enable_if_t<!is_same<C, decay_t<P>>
zu den template-parameter-Liste), um sicherzustellen, dass es keinen Zusammenstoß mit der copy/move-Konstruktoren (auch wenn letztere werden explizit vom Benutzer bereitgestellt)?constexpr
Daten-member deklariert, die in einer Klasse ohne ein out-of-class-definition, wenn es verwendet wird, als argument für einen parameter der lvalue-Referenz-Typ kann es schließlich nicht verlinken, weil es ist OS-verwendet und es gibt keine definition.constexpr
Datenelement geändert haben die Einführung einer definition implizit, so ist der Unterschied nicht signifikant ist in diesem Fall.Die Nachteile sind:
noexcept
-Bezeichner für die Parameter derconst&
und&&
qualifizierten Typen.noexcept(false)
copy +noexcept
bewegen, wenn Sie gibtnoexcept
oder immernoexcept(false)
wenn Sie angeben, dass nichts (oder explizitenoexcept(false)
). (Hinweis: im ersten Fallnoexcept
nicht verhindern, dass das werfen beim kopieren, denn das wird nur auftreten, während der Auswertung von Argumenten, die aus der Funktion Körper.) Es gibt keine weitere chance, optimieren Sie Sie separat.noexcept
werden können, besonders problematisch, da C++17, weilnoexcept
-Spezifikation nun auf die Funktion geben. (Einige unerwartete Kompatibilitätsprobleme können diagnostiziert werden, durch Clang++ Warnung.)Manchmal die bedingungslose Kopie ist tatsächlich nützlich. Da die Komposition von Operationen mit starken Ausnahme-Garantie nicht über die Garantie in der Natur, eine Kopie kann verwendet werden, als eine transnationale, Staatliche Halter, wenn der starke-Ausnahme-Garantie ist erforderlich und der Vorgang kann nicht zerbrochen werden, als Folge von Operationen mit nicht weniger strenge (nicht-Ausnahme oder stark) Ausnahme-Garantie. (Dies beinhaltet die copy-and-swap-idiom, obwohl die Zuordnungen sind nicht empfohlen vereint zu sein für andere Gründe, im Allgemeinen, siehe unten.) Jedoch, dies bedeutet nicht das kopieren auf andere Weise inakzeptabel ist. Wenn die Absicht, die Schnittstelle ist immer zu erstellen, die ein Objekt vom Typ
T
, und die Kosten der VerlagerungT
ist vernachlässigbar, die Kopie kann verschoben werden, um das Ziel ohne unerwünschte overhead.Schlussfolgerungen
Also für einige bestimmten Operationen, hier sind Vorschläge, ob mit einer Vereinheitlichung der Schnittstelle zu ersetzen:
T
, wenn Sie eine Kopie jedes argument wird benötigt für alle Operationen verwenden Vereinheitlichung.T
haben ignorable Kosten, nutzen vereinen.T
, und die Kosten für den Umzug BauT
ist vernachlässigbar, verwenden Vereinheitlichung.Hier sind einige Beispiele, die Notwendigkeit zu vermeiden, Vereinheitlichung:
T
ohne vernachlässigbar Kosten, die in kopieren und verschieben Konstruktionen erfüllt nicht die Kriterien für die Vereinheitlichung, weil die intention des Auftrags ist nicht zu erstellen (aber ersetzen den Inhalt) des Objekts. Das kopierte Objekt wird irgendwann zerstört werden, das verursacht unnötigen Aufwand. Dies wird noch deutlicher, für Fälle der selbst-Zuordnung.std::map::insert_or_assign
-wie container einfügen, auch trotz der Fehler oben).Hinweis: die genaue Grenze des "ignorable" Kosten ist etwas subjektiv, weil es letztendlich davon abhängt, wie viel Kosten toleriert werden können, von den Entwicklern und/oder den Benutzer, und es kann variieren von Fall zu Fall.
Praktisch, ich (konservativ) übernimmt keinerlei trivial kopierbar und trivailly zerstörbare-Typ, dessen Größe nicht mehr als eine Maschine-Wort (wie ein Zeiger) Qualifikation, die Kriterien der ignorable Kosten in der Regel - wenn der resultierende code tatsächlich zu viel Kosten, in diesem Fall schlägt es vor, entweder eine falsche Konfiguration des build-tool verwendet wird, oder die toolchain ist nicht bereit für die Produktion.
Tun Profil, wenn es keine weiteren Zweifel an der Leistung.
Zusätzliche Fallstudie
Gibt es einige andere bekannte Arten bevorzugt werden übergeben von Werten konventionell:
std::move
, weil die Kosten für die kopieren wird angenommen, vernachlässigbar und ein Umzug nicht unbedingt machen es besser. Solche Parameter umfassen Iteratoren und Funktions-Objekte (ausser im Falle von (argument) die Weiterleitung der Anrufer-Wrapper).std::initializer_list
undstd::basic_string_view
sind mehr oder weniger zwei Zeiger oder einen Zeiger oder plus Größe. Diese Tatsache macht Sie Billig genug, um direkt übergeben, ohne Referenzen.Ebenso einige Typen sollten vermeiden Weitergabe von Werten, es sei denn, Sie benötigen eine Kopie. Container sind typische Beispiele in dieser Art.
* Insbesondere Zuweisung-awared Container und einige andere Arten mit ähnlichen Behandlung zu allocators ("container-Semantik", in David Krauss' Wort), sollte nicht per value übergeben werden, auch wenn die Leistung ist völlig uninteressiert - Zuweisung Vermehrung ist nur ein weiterer großer semantischer Wurm.
Ein paar andere Arten, die konventionell hängt. Zum Beispiel, sehen Sie GotW #91 für
shared_ptr
Instanzen. (Allerdings nicht alle smart-Pointer sind so;observer_ptr
mehr wie ein raw-Pointer.)Durch Wert übergeben, dann verschieben ist eigentlich eine gute Ausdrucksweise ist für Objekte, die Sie kennen, sind beweglich.
Wie Sie bereits erwähnt, wenn ein rvalue ist bestanden, werde es entweder zu umgehen, aber die Kopie, oder verschoben werden, dann innerhalb des Konstruktors wird es verschoben.
Sie könnten zu einer überlastung der copy-Konstruktor und move-Konstruktor explizit, aber es wird komplizierter, wenn Sie mehr als einen parameter.
Betrachten Sie das Beispiel,
Angenommen, wenn Sie wollte expliziten Versionen, die Sie am Ende mit 4 Konstruktoren-wie so:
Wie Sie sehen können, wie erhöhen Sie die Anzahl der Parameter, die Anzahl der notwendigen Konstruktoren wachsen in Permutationen.
Wenn Sie nicht über einen konkreten Typ, sondern haben eine templatized Konstruktor, den Sie verwenden können, um perfekte Weiterleitung in etwa so:
Referenzen:
Ich beantworte mich selbst, denn ich werde versuchen, das zusammenzufassen, einige der Antworten. Wie viele Züge/Kopien haben wir in jedem Fall?
(A) Pass-by-value und bewegen Zuordnung konstruieren, vorbei an einem X-parameter. Wenn X eine...
Vorübergehend: 1 bewegen Sie (die Kopie erstellte)
Lvalue: 1 kopieren 1 bewegen
std::move(lvalue): 2 bewegt
(B) übergabe per Referenz und Kopie Zuweisung üblichen (vor C++11) zu konstruieren. Wenn X eine...
Vorübergehend: 1 Kopie
Lvalue: 1 Kopie
std::move(lvalue): 1-Kopie
Können wir davon ausgehen, die drei Arten von Parametern sind gleich wahrscheinlich. So alle 3 Anrufe, die wir haben (A) mit 4 Zügen und 1 Kopie, oder (B) 3 Exemplare. I. e., im Durchschnitt, (A) 1.33 bewegt und 0,33 Kopien pro Anruf oder (B) 1 Kopie pro Aufruf.
Wenn wir kommen, um eine situation, bei der unsere Klassen bestehen meist aus Hülsen, Züge sind so teuer wie Kopien. So hätten wir 1.66 kopiert (oder verschiebt) pro Aufruf der setter-im Fall (A) und 1-Kopien im Fall (B).
Können wir sagen, dass in einigen Fällen ("PODs" Basis-Typen), die pass-by-value-and-dann-bewegen konstruieren, ist eine sehr schlechte Idee. Es ist 66% langsamer und es hängt ein C++11 feature.
Auf der anderen Seite, wenn unsere Klassen umfassen Container (die Verwendung von dynamischen Speicher), (A) soll deutlich schneller sein (außer wenn wir meistens pass lvalues).
Bitte, korrigieren Sie mich, wenn ich falsch bin.
Lesbarkeit in der Erklärung:
Leistung:
Aufgaben:
Für typische inline-code gibt es meist keinen Unterschied, wenn Sie optimiert.
Aber foo2() kann das kopieren nur unter bestimmten Bedingungen (z.B. einfügen in die map, wenn Schlüssel nicht vorhanden), in der Erwägung, dass für foo1() die Kopie wird immer gemacht werden.