Die Vorteile von Pass-by-Value und std :: bewegen sich über Pass-by-Reference

105

Ich lerne gerade C ++ und versuche, schlechte Gewohnheiten zu vermeiden. Soweit ich weiß, enthält clang-tidy viele "Best Practices" und ich versuche, mich so gut wie möglich an sie zu halten (obwohl ich nicht unbedingt verstehe, warum sie noch als gut gelten), aber ich bin mir nicht sicher, ob ich es tue Verstehe, was hier empfohlen wird.

Ich habe diese Klasse aus dem Tutorial verwendet:

class Creature
{
private:
    std::string m_name;

public:
    Creature(const std::string &name)
            :  m_name{name}
    {
    }
};

Dies führt zu einem Vorschlag von clang-tidy, dass ich Wert anstelle von Referenz und Verwendung übergeben soll std::move. Wenn ich das tue, erhalte ich den Vorschlag zu machen , nameeine Referenz (um sicherzustellen , dass nicht jedes Mal kopiert bekommt) und die Warnung , dass std::movekeine Auswirkungen haben , da nameeine ist , constdamit ich es entfernen sollte.

Die einzige Möglichkeit, keine Warnung zu erhalten, besteht darin, sie constvollständig zu entfernen :

Creature(std::string name)
        :  m_name{std::move(name)}
{
}

Was logisch erscheint, da der einzige Vorteil darin constbestand, das Durcheinander mit der ursprünglichen Zeichenfolge zu verhindern (was nicht passiert, weil ich den Wert übergeben habe). Aber ich habe auf CPlusPlus.com gelesen :

Beachten Sie jedoch, dass das Verschieben in der Standardbibliothek impliziert, dass das verschobene Objekt in einem gültigen, aber nicht angegebenen Zustand belassen wird. Dies bedeutet, dass nach einer solchen Operation der Wert des verschobenen Objekts nur zerstört oder ein neuer Wert zugewiesen werden sollte. Wenn Sie andernfalls darauf zugreifen, erhalten Sie einen nicht angegebenen Wert.

Stellen Sie sich nun diesen Code vor:

std::string nameString("Alex");
Creature c(nameString);

Da nameStringder Wert übergeben wird, std::movewird er nur nameinnerhalb des Konstruktors ungültig und berührt nicht die ursprüngliche Zeichenfolge. Aber was sind die Vorteile davon? Es scheint, als würde der Inhalt sowieso nur einmal kopiert - wenn ich beim Aufrufen als Referenz übergebe m_name{name}, wenn ich beim Übergeben den Wert übergebe (und dann wird er verschoben). Ich verstehe, dass dies besser ist, als den Wert zu übergeben und nicht zu verwenden std::move(weil es zweimal kopiert wird).

Also zwei Fragen:

  1. Habe ich richtig verstanden, was hier passiert?
  2. Gibt es einen Vorteil std::move, wenn Sie das Übergeben als Referenz verwenden und nur anrufen m_name{name}?
Blackbot
quelle
3
Mit Pass by Reference Creature c("John");macht eine zusätzliche Kopie
user253751
1
Dieser Link könnte eine wertvolle Lektüre sein, er behandelt auch das Übergeben std::string_viewund SSO.
lubgr
Ich habe festgestellt, dass dies clang-tidyeine großartige Möglichkeit ist, mich auf Kosten der Lesbarkeit von unnötigen Mikrooptimierungen besessen zu machen. Die Frage, die hier vor allem gestellt werden muss, ist, wie oft wir den Konstruktor tatsächlich aufrufen Creature.
cz

Antworten:

36
  1. Habe ich richtig verstanden, was hier passiert?

Ja.

  1. Gibt es einen Vorteil std::move, wenn Sie das Übergeben als Referenz verwenden und nur anrufen m_name{name}?

Eine leicht zu erfassende Funktionssignatur ohne zusätzliche Überlastungen. Die Signatur zeigt sofort, dass das Argument kopiert wird - dies erspart Anrufern die Frage, ob eine const std::string&Referenz als Datenelement gespeichert werden könnte, und wird möglicherweise später zu einer baumelnden Referenz. Und es gibt keine Notwendigkeit , um eine Überlastung auf std::string&& nameund const std::string&Argumente unnötige Kopien zu vermeiden , wenn rvalues an die Funktion übergeben werden. Einen Wert übergeben

std::string nameString("Alex");
Creature c(nameString);

Die Funktion, die ihr Argument nach Wert nimmt, verursacht eine Kopie und eine Verschiebungskonstruktion. Übergabe eines rWertes an dieselbe Funktion

std::string nameString("Alex");
Creature c(std::move(nameString));

verursacht zwei Bewegungskonstruktionen. Im Gegensatz dazu gibt es beim Funktionsparameter const std::string&immer eine Kopie, auch wenn ein rvalue-Argument übergeben wird. Dies ist eindeutig ein Vorteil, solange der Argumenttyp billig zu verschieben ist (dies ist der Fall für std::string).

Es ist jedoch ein Nachteil zu berücksichtigen: Die Argumentation funktioniert nicht für Funktionen, die das Funktionsargument einer anderen Variablen zuweisen (anstatt es zu initialisieren):

void setName(std::string name)
{
    m_name = std::move(name);
}

führt zu einer Freigabe der Ressource, m_nameauf die verwiesen wird, bevor sie neu zugewiesen wird. Ich empfehle, Punkt 41 in Effective Modern C ++ und auch diese Frage zu lesen .

lubgr
quelle
Das ist sinnvoll, insbesondere, weil dadurch die Erklärung intuitiver zu lesen ist. Ich bin nicht sicher, ob ich den Teil der Freigabe Ihrer Antwort vollständig verstehe (und den verknüpften Thread verstehe). Nur um zu überprüfen, ob ich ihn verwende move, wird der Speicherplatz freigegeben. Wenn ich nicht verwende move, wird die Zuordnung nur freigegeben, wenn der zugewiesene Speicherplatz zu klein ist, um die neue Zeichenfolge aufzunehmen, was zu einer verbesserten Leistung führt. Ist das korrekt?
Blackbot
1
Ja, genau das ist es. Bei der Zuweisung von m_nameaus einem const std::string&Parameter wird der interne Speicher so lange wiederverwendet, wie er m_namepasst. Bei der Zuweisung von Verschiebungen m_namemuss der Speicher zuvor freigegeben werden. Andernfalls war es unmöglich, die Ressourcen von der rechten Seite der Aufgabe zu "stehlen".
lubgr
Wann wird es zu einer baumelnden Referenz? Ich denke, Initialisierungsliste verwenden tiefe Kopie.
Li Taiji
99
/* (0) */ 
Creature(const std::string &name) : m_name{name} { }
  • Eine bestandene lvalue bindet name, wird dann kopiert in m_name.

  • Eine bestandene rvalue bindet name, wird dann kopiert in m_name.


/* (1) */ 
Creature(std::string name) : m_name{std::move(name)} { }
  • Eine bestandene lvalue wird kopiert inname , dann wird bewegt in m_name.

  • A geleitet rvalue wird bewegt in name, wird bewegt in m_name.


/* (2) */ 
Creature(const std::string &name) : m_name{name} { }
Creature(std::string &&rname) : m_name{std::move(rname)} { }
  • Ein übergebener Wert bindet anname, wird dann kopiert in m_name.

  • Ein übergebener Wert bindet an rnameund wird dann verschoben , in m_name.


Da Verschiebungsvorgänge normalerweise schneller sind als Kopien, (1) besser als (0), wenn Sie viele Provisorien übergeben. (2) ist in Bezug auf Kopien / Verschiebungen optimal, erfordert jedoch eine Code-Wiederholung.

Die Code-Wiederholung kann mit vermieden werden perfekte Weiterleitung :

/* (3) */
template <typename T,
          std::enable_if_t<
              std::is_convertible_v<std::remove_cvref_t<T>, std::string>, 
          int> = 0
         >
Creature(T&& name) : m_name{std::forward<T>(name)} { }

Möglicherweise möchten Sie optional einschränken T einschränken, um die Domäne der Typen einzuschränken, mit denen dieser Konstruktor instanziiert werden kann (wie oben gezeigt). C ++ 20 soll dies mit Concepts vereinfachen .


In C ++ 17 sind pr-Werte von einer garantierten Kopierentfernung betroffen , die - falls zutreffend - die Anzahl der Kopien / Verschiebungen verringert, wenn Argumente an Funktionen übergeben werden.

Vittorio Romeo
quelle
Für (1) sind der Fall pr-value und xvalue nicht identisch, da c ++ 17 no?
Oliv
1
Beachten Sie, dass Sie die SFINAE in diesem Fall nicht benötigen, um vorwärts zu perfektionieren. Es ist nur zur Disambiguierung erforderlich. Es ist plausibel hilfreich für die möglichen Fehlermeldungen beim Übergeben von schlechten Argumenten
Caleth
@Oliv Ja. x-Werte müssen verschoben werden, während Werte entfernt werden können :)
Rakete1111
1
Können wir schreiben: Creature(const std::string &name) : m_name{std::move(name)} { }in die (2) ?
Skytree
4
@skytree: Sie können sich nicht von einem const-Objekt entfernen, da das Verschieben die Quelle mutiert. Das wird kompiliert, aber es wird eine Kopie erstellt.
Vittorio Romeo
1

Wie Sie bestehen, ist hier nicht die einzige Variable. Was Sie bestehen, macht den großen Unterschied zwischen den beiden.

In C ++, haben wir alle Arten von Wertkategorie und dieses „Idiom“ gibt sich für Fälle , in denen Sie in einem Pass rvalue (wie "Alex-string-literal-that-constructs-temporary-std::string"oder std::move(nameString)), die Ergebnisse in 0 Kopien von std::stringgemacht werden (die Art auch dann nicht haben copy-konstruierbar sein für rvalue-Argumente) und verwendet nur std::stringden Verschiebungskonstruktor.

Etwas verwandte Fragen und Antworten .

LogicStuff
quelle
1

Es gibt mehrere Nachteile des Pass-by-Value-and-Move-Ansatzes gegenüber der Pass-by- (rv) -Referenz:

  • es werden 3 Objekte anstelle von 2 erzeugt;
  • Das Übergeben eines Objekts als Wert kann zu einem zusätzlichen Stapel-Overhead führen, da selbst die reguläre Zeichenfolgenklasse in der Regel mindestens drei- oder viermal größer als ein Zeiger ist.
  • Die Konstruktion von Argumentobjekten wird auf der Aufruferseite durchgeführt, wodurch der Code aufgebläht wird.
user7860670
quelle
Könnten Sie klarstellen, warum dadurch 3 Objekte erscheinen? Soweit ich weiß, kann ich "Peter" einfach als Zeichenfolge übergeben. Dies würde erzeugt, kopiert und dann verschoben werden, nicht wahr? Und würde der Stapel nicht irgendwann trotzdem verwendet werden? Nicht am Punkt des Konstruktoraufrufs, sondern an dem m_name{name}Teil, an dem er kopiert wird?
Blackbot
@ Blackbot Ich habe mich auf Ihr Beispiel bezogen. std::string nameString("Alex"); Creature c(nameString);Ein Objekt ist ein nameStringanderes, ein Funktionsargument und das dritte ist ein Klassenfeld.
user7860670