it-swarm.com.de

Sollen Funktionsparameter zur Unterstützung der Verschiebungs-Semantik von unique_ptr, von value oder von rvalue verwendet werden?

Eine meiner Funktionen nimmt einen Vektor als Parameter und speichert ihn als Elementvariable. Ich verwende den const-Verweis auf einen Vektor wie unten beschrieben.

class Test {
 public:
  void someFunction(const std::vector<string>& items) {
   m_items = items;
  }

 private:
  std::vector<string> m_items;
};

Manchmal enthält items jedoch eine große Anzahl von Zeichenfolgen. Daher möchte ich eine Funktion hinzufügen (oder die Funktion durch eine neue ersetzen), die die Semantik des Verschiebens unterstützt.

Ich denke an verschiedene Ansätze, aber ich bin mir nicht sicher, welche ich wählen soll.

1) unique_ptr 

void someFunction(std::unique_ptr<std::vector<string>> items) {
   // Also, make `m_itmes` std::unique_ptr<std::vector<string>>
   m_items = std::move(items);
}

2) durch Wert gehen und bewegen

void someFunction(std::vector<string> items) {
   m_items = std::move(items);
}

3) Wert

void someFunction(std::vector<string>&& items) {
   m_items = std::move(items);
}

Welchen Ansatz sollte ich vermeiden und warum?

17
MaxHeap

Wenn Sie keinen Grund für den Vektor haben, auf dem Heap zu leben, würde ich raten, unique_ptr zu verwenden.

Der interne Speicher des Vektors ist ohnehin auf dem Heapspeicher vorhanden. Wenn Sie unique_ptr verwenden, benötigen Sie 2 Umlenkungsgrade, um den Zeiger auf den Vektor zu demeferenzieren und erneut den internen Speicherpuffer zu entreissen.

Daher würde ich empfehlen, entweder 2 oder 3 zu verwenden.

Wenn Sie mit Option 3 fortfahren (eine rvalue-Referenz erforderlich), fordern Sie die Benutzer Ihrer Klasse auf, einen rvalue zu übergeben (entweder direkt von einem temporären Wert oder von einem lvalue aus), wenn Sie someFunction aufrufen.

Die Anforderung, sich von einem Wert zu bewegen, ist sehr hoch.

Wenn Ihre Benutzer eine Kopie des Vektors behalten möchten, müssen sie dazu durch die Rahmen springen.

std::vector<string> items = { "1", "2", "3" };
Test t;
std::vector<string> copy = items; // have to copy first
t.someFunction(std::move(items));

Wenn Sie jedoch mit Option 2 fortfahren, kann der Benutzer entscheiden, ob er eine Kopie behalten möchte oder nicht - die Wahl liegt bei ihnen

Behalte eine Kopie:

std::vector<string> items = { "1", "2", "3" };
Test t;
t.someFunction(items); // pass items directly - we keep a copy

Bewahren Sie keine Kopie auf:

std::vector<string> items = { "1", "2", "3" };
Test t;
t.someFunction(std::move(items)); // move items - we don't keep a copy
30
Steve Lorimer

Auf den ersten Blick scheint Option 2 eine gute Idee zu sein, da sowohl l- als auch r-Werte in einer einzigen Funktion behandelt werden. Wie Herb Sutter jedoch in seinem CppCon 2014-Vortrag festhält: Zurück zu den Grundlagen! Grundlagen des modernen C++ - Stils ist dies ein Pessimisierung für den allgemeinen Fall von lWerten.

Wenn m_items war "größer" als items, Ihr ursprünglicher Code weist dem Vektor keinen Speicher zu:

// Original code:
void someFunction(const std::vector<string>& items) {
   // If m_items.capacity() >= items.capacity(),
   // there is no allocation.
   // Copying the strings may still require
   // allocations
   m_items = items;
}

Der Kopierzuweisungsoperator für std::vector ist intelligent genug, um die vorhandene Zuordnung wiederzuverwenden. Wenn Sie den Parameter hingegen nach Wert nehmen, muss immer eine andere Zuordnung vorgenommen werden:

// Option 2:
// When passing in an lvalue, we always need to allocate memory and copy over
void someFunction(std::vector<string> items) {
   m_items = std::move(items);
}

Einfach ausgedrückt: Kopieraufbau und Kopierzuordnung müssen nicht unbedingt die gleichen Kosten verursachen. Es ist nicht unwahrscheinlich, dass die Zuweisung von Kopien effizienter ist als die Erstellung von Kopien - sie ist effizienter für std::vector und std::string .

Die einfachste Lösung ist, wie Herb feststellt, eine R-Wert-Überladung hinzuzufügen (im Grunde Ihre Option 3):

// You can add `noexcept` here because there will be no allocation‡
void someFunction(std::vector<string>&& items) noexcept {
   m_items = std::move(items);
}

Beachten Sie, dass die Kopierzuweisungsoptimierung nur funktioniert, wenn m_items existiert bereits, daher ist es völlig in Ordnung, Konstruktoren nach Wert zu übergeben - die Zuordnung müsste in beiden Fällen erfolgen.

TL; DR: Wählen Sie Hinzufügen Option 3. Das heißt, Sie haben eine Überladung für lWerte und eine für rWerte. Option 2 erzwingt Kopie Konstruktion anstelle von Kopie Zuordnung, was teurer sein kann (und für std::string und std::vector)

† Wenn Sie Benchmarks sehen möchten, die zeigen, dass Option 2 eine Pessimisierung sein kann, an diesem Punkt im Vortrag , zeigt Herb einige Benchmarks

‡ Wir hätten dies nicht als noexcept markieren sollen, wenn std::vectors Operator für die Zuweisung von Zügen war nicht noexcept. Konsultieren Sie die Dokumentation , wenn Sie einen benutzerdefinierten Allokator verwenden.
Als Faustregel gilt, dass ähnliche Funktionen nur dann als noexcept markiert werden sollten, wenn die Bewegungszuweisung des Typs noexcept lautet.

14
Justin

Das hängt von Ihren Nutzungsmustern ab:

Option 1

Pros: 

  • Die Verantwortung wird ausdrücklich zum Ausdruck gebracht und vom Anrufer an den Angerufenen weitergegeben

Nachteile:

  • Wenn der Vektor nicht bereits mit einem unique_ptr umbrochen wurde, wird die Lesbarkeit dadurch nicht verbessert
  • Intelligente Zeiger verwalten im Allgemeinen dynamisch zugewiesene Objekte. Ihre vector muss also eins werden. Da Standard-Bibliothekscontainer verwaltete Objekte sind, die interne Zuordnungen zum Speichern ihrer Werte verwenden, bedeutet dies, dass es für jeden Vektor zwei dynamische Zuordnungen gibt. Einer für den Verwaltungsblock des eindeutigen ptr + des vector-Objekts selbst und ein zusätzlicher für die gespeicherten Elemente.

Zusammenfassung:

Wenn Sie diesen Vektor konsistent mit einem unique_ptr verwalten, verwenden Sie ihn weiter, andernfalls nicht.

Option 2

Pros: 

  • Diese Option ist sehr flexibel, da der Anrufer entscheiden kann, ob er eine Kopie behalten möchte oder nicht:

    std::vector<std::string> vec { ... };
    Test t;
    t.someFunction(vec); // vec stays a valid copy
    t.someFunction(std::move(vec)); // vec is moved
    
  • Wenn der Aufrufer std::move() verwendet, wird das Objekt nur zweimal verschoben (keine Kopien), was effizient ist.

Nachteile:

  • Wenn der Aufrufer std::move() nicht verwendet, wird immer ein Kopierkonstruktor aufgerufen, um das temporäre Objekt zu erstellen. Wenn wir void someFunction(const std::vector<std::string> & items) verwenden würden und unser m_items bereits groß genug wäre (hinsichtlich der Kapazität), um items Platz zu bieten, wäre die Zuweisung m_items = items nur ein Kopiervorgang ohne die zusätzliche Zuweisung gewesen.

Zusammenfassung:

Wenn Sie im Voraus wissen, dass dieses Objekt zur Laufzeit mehrmals re -set sein wird und der Aufrufer std::move() nicht immer verwendet, hätte ich es vermieden. Ansonsten ist dies eine großartige Option, da sie sehr flexibel ist und trotz des problematischen Szenarios sowohl eine Benutzerfreundlichkeit als auch eine höhere Leistung bei Bedarf ermöglicht.

Option 3

Nachteile:

  • Diese Option zwingt den Anrufer, seine Kopie aufzugeben. Wenn er also eine Kopie für sich behalten möchte, muss er zusätzlichen Code schreiben:

    std::vector<std::string> vec { ... };
    Test t;
    t.someFunction(std::vector<std::string>{vec});
    

Zusammenfassung:

Dies ist weniger flexibel als Option # 2, und daher würde ich sagen, dass es in den meisten Szenarien schlechter ist.

Option 4

Angesichts der Nachteile der Optionen 2 und 3 würde ich eine zusätzliche Option vorschlagen:

void someFunction(const std::vector<int>& items) {
    m_items = items;
}

// AND

void someFunction(std::vector<int>&& items) {
    m_items = std::move(items);
}

Pros:

  • Es löst alle problematischen Szenarien, die für die Optionen 2 und 3 beschrieben wurden, und genießt deren Vorteile
  • Caller beschloss, eine Kopie für sich zu behalten oder nicht
  • Kann für jedes gegebene Szenario optimiert werden

Nachteile:

  • Wenn die Methode viele Parameter als Konstantenreferenzen und/oder rvalue-Referenzen akzeptiert, wächst die Anzahl der Prototypen exponentiell

Zusammenfassung:

Solange Sie keine solchen Prototypen haben, ist dies eine großartige Option.

6
Daniel Trugman

Der aktuelle Hinweis dazu ist, den Vektor nach Wert zu nehmen und in die Membervariable zu verschieben:

void fn(std::vector<std::string> val)
{
  m_val = std::move(val);
}

Und ich habe gerade überprüft, dass std::vector einen Verschiebungszuweisungsoperator liefert. Wenn der Anrufer keine Kopie behalten möchte, kann er diese in die Funktion auf der Anrufseite verschieben: fn(std::move(vec));.

0
Andre Kostur