Ist Pass-by-Value in C ++ 11 ein vernünftiger Standard?

142

In herkömmlichem C ++ ist die Übergabe von Werten an Funktionen und Methoden für große Objekte langsam und wird im Allgemeinen verpönt. Stattdessen neigen C ++ - Programmierer dazu, Referenzen weiterzugeben, was schneller ist, aber alle möglichen komplizierten Fragen zum Besitz und insbesondere zur Speicherverwaltung aufwirft (falls das Objekt Heap-zugewiesen ist).

Jetzt haben wir in C ++ 11 Rvalue-Referenzen und Verschiebungskonstruktoren, was bedeutet, dass es möglich ist, ein großes Objekt (wie ein Objekt std::vector) zu implementieren , das kostengünstig als Wert in eine Funktion und aus einer Funktion übergeben werden kann.

Bedeutet dies also, dass der Standardwert für Instanzen von Typen wie std::vectorund als Wert übergeben werden sollte std::string? Was ist mit benutzerdefinierten Objekten? Was ist die neue Best Practice?

Derek Thurn
quelle
22
pass by reference ... which introduces all sorts of complicated questions around ownership and especially around memory management (in the event that the object is heap-allocated). Ich verstehe nicht, wie kompliziert oder problematisch es für das Eigentum ist? Kann ich etwas verpasst haben?
Iammilind
1
@iammilind: Ein Beispiel aus persönlicher Erfahrung. Ein Thread hat ein String-Objekt. Es wird an eine Funktion übergeben, die einen anderen Thread erzeugt, dem Aufrufer jedoch unbekannt ist, dass die Funktion die Zeichenfolge als const std::string&und nicht als Kopie verwendet hat. Der erste Thread wurde dann beendet ...
Zan Lynx
12
@ZanLynx: Das klingt nach einer Funktion, die eindeutig nie als Thread-Funktion aufgerufen wurde.
Nicol Bolas
5
Wenn ich mit iammilind einverstanden bin, sehe ich kein Problem. Die Übergabe als const-Referenz sollte Ihre Standardeinstellung für "große" Objekte und als Wert für kleinere Objekte sein. Ich würde die Grenze zwischen groß und klein auf ungefähr 16 Bytes (oder 4 Zeiger auf einem 32-Bit-System) setzen.
JN
3
Herb Sutter ist zurück zu den Grundlagen! Die Grundlagen der modernen C ++ - Präsentation auf der CppCon wurden hier ausführlich behandelt. Video hier .
Chris Drew

Antworten:

138

Dies ist eine vernünftige Standardeinstellung, wenn Sie eine Kopie im Text erstellen müssen. Dies ist, was Dave Abrahams befürwortet :

Richtlinie: Kopieren Sie Ihre Funktionsargumente nicht. Übergeben Sie sie stattdessen als Wert und lassen Sie den Compiler das Kopieren durchführen.

Im Code bedeutet dies, dass Sie Folgendes nicht tun:

void foo(T const& t)
{
    auto copy = t;
    // ...
}

aber mach das:

void foo(T t)
{
    // ...
}

Das hat den Vorteil, dass der Anrufer foowie folgt nutzen kann:

T lval;
foo(lval); // copy from lvalue
foo(T {}); // (potential) move from prvalue
foo(std::move(lval)); // (potential) move from xvalue

und es wird nur minimale Arbeit geleistet. Sie benötigen zwei Überladungen, um dasselbe mit Referenzen zu tun, void foo(T const&);und void foo(T&&);.

In diesem Sinne habe ich jetzt meine geschätzten Konstruktoren als solche geschrieben:

class T {
    U u;
    V v;
public:
    T(U u, V v)
        : u(std::move(u))
        , v(std::move(v))
    {}
};

Andernfalls constist es vernünftig, auf still zu verweisen .

Luc Danton
quelle
29
+1, insbesondere für das letzte Bit :) Man sollte nicht vergessen, dass Verschiebungskonstruktoren nur aufgerufen werden können, wenn nicht erwartet wird, dass das Objekt, von dem verschoben werden soll, danach unverändert SomeProperty p; for (auto x: vec) { x.foo(p); }bleibt : passt beispielsweise nicht. Außerdem haben Verschiebungskonstruktoren Kosten (je größer das Objekt, desto höher die Kosten), während sie const&im Wesentlichen kostenlos sind.
Matthieu M.
25
@MatthieuM. Es ist jedoch wichtig zu wissen, was "je größer das Objekt, desto höher die Kosten" des Umzugs tatsächlich bedeutet: "größer" bedeutet tatsächlich "je mehr Mitgliedsvariablen es hat". Zum Beispiel kostet das Verschieben eines std::vectormit einer Million Elementen dasselbe wie das Verschieben eines mit fünf Elementen, da nur der Zeiger auf das Array auf dem Heap verschoben wird, nicht jedes Objekt im Vektor. Es ist also eigentlich kein so großes Problem.
Lucas
+1 Ich neige auch dazu, das Konstrukt "Pass-by-Value-Then-Move" zu verwenden, seit ich C ++ 11 verwende. Dies macht mich allerdings etwas unruhig, da mein Code jetzt std::moveüberall ist ..
stijn
1
Es gibt ein Risiko const&, das mich ein paar Mal gestolpert hat. void foo(const T&); int main() { S s; foo(s); }. Dies kann kompiliert werden, obwohl die Typen unterschiedlich sind, wenn es einen T-Konstruktor gibt, der ein S als Argument verwendet. Dies kann langsam sein, da möglicherweise ein großes T-Objekt erstellt wird. Sie könnten denken, Sie würden eine Referenz übergeben, ohne sie zu kopieren, aber Sie könnten es sein. Siehe diese Antwort auf eine Frage, die ich nach mehr gefragt habe . Grundsätzlich &bindet normalerweise nur an lWerte, aber es gibt eine Ausnahme für rvalue. Es gibt Alternativen.
Aaron McDaid
1
@AaronMcDaid Dies sind alte Nachrichten in dem Sinne, dass Sie dies schon vor C ++ 11 immer beachten mussten. Und daran hat sich nicht viel geändert.
Luc Danton
71

In fast allen Fällen sollte Ihre Semantik entweder wie folgt sein:

bar(foo f); // want to obtain a copy of f
bar(const foo& f); // want to read f
bar(foo& f); // want to modify f

Alle anderen Unterschriften sollten nur sparsam und mit guter Begründung verwendet werden. Der Compiler wird diese nun so gut wie immer auf die effizienteste Weise herausarbeiten. Sie können einfach mit dem Schreiben Ihres Codes fortfahren!

Ayjay
quelle
2
Obwohl ich es vorziehe, einen Zeiger zu übergeben, wenn ich ein Argument ändern möchte. Ich stimme dem Google Style Guide zu, dass dies offensichtlicher macht, dass das Argument geändert wird, ohne dass die Signatur der Funktion überprüft werden muss ( google-styleguide.googlecode.com/svn/trunk/… ).
Max Lybbert
40
Der Grund, warum ich es nicht mag, Zeiger zu übergeben, ist, dass es meinen Funktionen einen möglichen Fehlerzustand hinzufügt. Ich versuche, alle meine Funktionen so zu schreiben, dass sie nachweislich korrekt sind, da dies den Platz zum Verstecken von Fehlern erheblich verringert. Das foo(bar& x) { x.a = 3; }ist viel zuverlässiger (und lesbarer!) Alsfoo(bar* x) {if (!x) throw std::invalid_argument("x"); x->a = 3;
Ayjay
22
@ Max Lybbert: Mit einem Zeigerparameter müssen Sie nicht die Signatur der Funktion überprüfen, sondern Sie müssen die Dokumentation überprüfen, um festzustellen, ob Sie Nullzeiger übergeben dürfen, ob die Funktion Eigentümer wird usw. IMHO, Ein Zeigerparameter vermittelt viel weniger Informationen als eine nicht konstante Referenz. Ich stimme jedoch zu, dass es schön wäre, an der Aufrufstelle einen visuellen Hinweis darauf zu haben, dass das Argument geändert werden kann (wie das refSchlüsselwort in C #).
Luc Touraille
In Bezug auf die Übergabe von Werten und die Verwendung der Bewegungssemantik sind diese drei Optionen meiner Meinung nach besser geeignet, um die beabsichtigte Verwendung des Parameters zu erklären. Dies sind die Richtlinien, denen ich auch immer folge.
Trevor Hickey
1
@AaronMcDaid Diese is shared_ptr intended to never be null? Much as (I think) unique_ptr is?beiden Annahmen sind falsch. unique_ptrund shared_ptrkann null / nullptrWerte enthalten. Wenn Sie sich keine Gedanken über Nullwerte machen möchten, sollten Sie Referenzen verwenden, da diese niemals Null sein können. Sie müssen auch nicht tippen ->, was Sie als ärgerlich empfinden :)
Julian
10

Übergeben Sie Parameter als Wert, wenn Sie innerhalb des Funktionskörpers eine Kopie des Objekts benötigen oder nur das Objekt verschieben müssen. Übergeben const&Sie, wenn Sie nur nicht mutierenden Zugriff auf das Objekt benötigen.

Beispiel für eine Objektkopie:

void copy_antipattern(T const& t) { // (Don't do this.)
    auto copy = t;
    t.some_mutating_function();
}

void copy_pattern(T t) { // (Do this instead.)
    t.some_mutating_function();
}

Beispiel für Objektverschiebung:

std::vector<T> v; 

void move_antipattern(T const& t) {
    v.push_back(t); 
}

void move_pattern(T t) {
    v.push_back(std::move(t)); 
}

Beispiel für einen nicht mutierenden Zugriff:

void read_pattern(T const& t) {
    t.some_const_function();
}

Weitere Informationen finden Sie in diesen Blog-Beiträgen von Dave Abrahams und Xiang Fan .

Edward Brey
quelle
0

Die Signatur einer Funktion sollte den Verwendungszweck widerspiegeln. Die Lesbarkeit ist auch für den Optimierer wichtig.

Dies ist die beste Voraussetzung für einen Optimierer, um den schnellsten Code zu erstellen - zumindest theoretisch und wenn nicht in der Realität, dann in einigen Jahren Realität.

Leistungsüberlegungen werden im Zusammenhang mit der Parameterübergabe sehr oft überbewertet. Perfekte Weiterleitung ist ein Beispiel. Funktionen wie emplace_backsind meistens sehr kurz und sowieso inline.

Patrick Fromberg
quelle