Idiom für die Einhaltung von propagate_on_copy_assignment in Container ohne Allokator

10

Angenommen, Sie haben einen Container, der intern andere Standardcontainer verwendet, um komplexere Datenstrukturen zu bilden. Zum Glück sind die Standardcontainer bereits so konzipiert, dass sie alle notwendigen Arbeiten ausführen, um sicherzustellen, dass die Zuweiser kopiert / zugewiesen werden.

Also, normalerweise, wenn wir einen Container c haben, und intern einen std::vector<int> , können wir einen Kopierzuweisungsoperator schreiben, der nur sagt:

%Vor%

Tatsächlich müssen wir das nicht einmal schreiben (weil es genau das ist, was der Standardkopiezuweisungsoperator tut), aber lassen Sie uns einfach sagen, dass in diesem Fall eine zusätzliche Logik erforderlich ist, die der Standardoperator nicht ausführen würde:

%Vor%

Also, in diesem Fall gibt es kein Problem, weil der Vektorzuweisungsoperator die ganze Arbeit für uns erledigt, um sicherzustellen, dass die Zuweiser korrekt kopiert oder nicht kopiert werden.

Aber ... was ist, wenn wir einen Vektor haben, den wir nicht einfach kopieren können? Angenommen, es ist ein Vektor von Zeigern zu einer anderen internen Struktur.

Angenommen, wir haben einen internen Vektor, der Zeiger enthält: std::vector<node*, Alloc>

Normalerweise müssten wir also in unserem Kopierzuweisungsoperator sagen:

%Vor%

Im obigen Beispiel müssen wir daher alle node -Objekte in m_vec manuell freigeben und dann neue Knotenobjekte aus dem RHS-Container erstellen. (Beachten Sie, dass ich das gleiche Zuordnungsobjekt verwende, das der Vektor intern verwendet, um Knotenobjekte zuzuordnen.)

Aber wenn wir hier und mit AllocatorAware standardkonform sein wollen, müssen wir prüfen, ob allocator_traits<std::vector<node*, Alloc>::allocator_type> propagate_on_container_copy_assign auf true setzt. Wenn dies der Fall ist, müssen wir den Zuordner des anderen Containers verwenden, um die kopierten Knoten zu erstellen.

Aber ... unser Containertyp Container verwendet keinen eigenen Zuordner. Es verwendet nur ein internes std::vector ... Wie können wir also unserer internen std::vector -Instanz mitteilen, dass sie bei Bedarf einen kopierten Allocator verwenden soll? Der Vektor hat nicht so etwas wie eine Elementfunktion "use_allocator" oder "set_allocator".

Also, das Einzige, was mir einfällt, ist etwas wie:

%Vor%

... und dann könnten wir unsere Knoten mit dem Rückgabewert von m_vec.get_allocator();

konstruieren

Ist dies ein gültiges Idiom zum Erstellen eines Zuweiser-bewußten Containers, der seinen eigenen Zuordner nicht behält, sondern auf einen internen Standardcontainer verweist?

    
Siler 15.12.2014, 23:18
quelle

2 Antworten

4

Ein Problem bei der Verwendung von swap zur Implementierung der Kopierzuweisung in diesem Beispiel besteht darin, dass, wenn propagate_on_assignment == true_type und propagate_on_container_swap == false_type , der Zuordner nicht von other nach *this weitergegeben wird, weil swap ablehnt mach es so.

Ein zweites Problem bei diesem Ansatz ist, dass wenn Sie propagate_on_assignment und propagate_on_container_swap == true_type aber other.m_vec.get_allocator() != m_vec.get_allocator() verwenden, Sie den Zuordner propagieren, aber Sie erhalten ein undefiniertes Verhalten am Punkt von swap .

Um das richtig zu machen, müssen Sie Ihre operator= wirklich von den ersten Principals entwerfen. Für diese Übung gehe ich davon aus, dass Container in etwa so aussieht:

%Vor%

i.e. Container wird auf T und Alloc gestempelt, und die Implementierung ermöglicht die Möglichkeit, dass Alloc "Fancy Pointer" verwendet (d. h. node* ist eigentlich ein Klassentyp).

In diesem Fall sieht der Operator Container copy wie folgt aus:

%Vor%

Erläuterung:

Um Speicher mit einem kompatiblen Allokator zuzuweisen und freizugeben, müssen wir std::allocator_traits verwenden, um ein " allocator<node> " zu erstellen. Im obigen Beispiel heißt das NodeAlloc . Es ist auch praktisch, Merkmale für diesen Allokator zu bilden, die NodeTraits oben genannt werden.

Der erste Job besteht aus einer konvertierten Kopie des lhs-Allokators (konvertiert von allocator<node*> in allocator<node> ) und verwendet diesen Allokator, um beide zu zerstören und die Zuordnung aufzuheben die LHs Knoten. std::addressof wird benötigt, um den möglicherweise "fancy pointer" in einen tatsächlichen node* im Aufruf von destroy zu konvertieren.

Als nächstes, und das ist etwas subtil, müssen wir m_vec.get_allocator() auf m_vec propagieren, aber nur, wenn propagate_on_container_copy_assignment wahr ist. Der Kopierzuweisungsoperator von vector ist der beste Weg, dies zu tun. Dies kopiert unnötigerweise einige NodePtr s, aber ich glaube immer noch, dass dies der beste Weg ist, diesen Zuordner zu propagieren. Wir könnten auch die Vektorzuweisung durchführen, wenn propagate_on_container_copy_assignment falsch ist, wodurch die if-Anweisung vermieden wird. Die Zuweisung würde den Allokator nicht weitergeben, wenn propagate_on_container_copy_assignment falsch ist, aber dann können wir noch NodePtr s zuweisen, wenn wir wirklich nur ein No-Op brauchen.

Wenn propagate_on_container_copy_assignment wahr ist und die beiden Zuordner ungleich sind, verarbeitet der Zuweisungsoperator vector copy korrekt die lhs-Ressourcen für uns, bevor die Zuordner zugewiesen werden. Dies ist eine Komplikation, die leicht übersehen werden kann und daher am besten dem Kopieroperator vector zugeordnet wird.

Wenn propagate_on_container_copy_assignment falsch ist, bedeutet das, dass wir uns nicht um den Fall kümmern müssen, in dem wir ungleiche Zuweiser haben. Wir werden keine Ressourcen austauschen.

In jedem Fall sollten wir, nachdem wir das getan haben, clear() the lhs. Dieser Vorgang löscht nicht capacity() und ist daher nicht verschwenderisch. An diesem Punkt haben wir eine Null-Größe Lhs mit dem richtigen Allokator und vielleicht sogar einige capacity() ungleich Null mit zu spielen.

Als Optimierung können wir reserve mit other.size() verwenden, falls die lhs-Kapazität nicht ausreicht. Diese Zeile ist für die Korrektheit nicht erforderlich. Es ist eine reine Optimierung.

Für den Fall, dass m_vec.get_allocator() jetzt einen neuen Zuordner zurückgibt, gehen wir vor und holen uns eine neue Kopie davon mit dem Namen alloc2 oben.

Wir können jetzt alloc2 verwenden, um neue Knoten zuzuteilen, zu konstruieren und zu speichern, die aus den rhs erstellt wurden.

Um ausnahmesicher zu sein, sollten wir ein RAII-Gerät verwenden, um den zugewiesenen Zeiger zu halten, während wir ihn konstruieren, und push_back in den Vektor. Entweder kann die Konstruktion werfen, wie auch die push_back() . Das RAII-Gerät muss sich darüber im Klaren sein, ob es in Ausnahmefällen eine Deallocation oder eine Destruction oder Deallocate durchführen muss. Das RAII-Gerät muss auch "schicker Zeiger" sein. Es stellt sich heraus, dass es sehr einfach ist, all dies mit std::unique_ptr in Kombination mit einem benutzerdefinierten Löschprogramm zu erstellen:

%Vor%

Beachten Sie die konsistente Verwendung von std::allocator_traits für alle Zugriffe auf den Allokator. Dadurch kann std::allocator_traits Standardwerte bereitstellen, sodass der Autor von Alloc diese nicht angeben muss. Zum Beispiel kann std::allocator_traits Standardimplementierungen für construct , destroy und propagate_on_container_copy_assignment bereitstellen.

Beachten Sie auch die konsistente Vermeidung der Annahme, dass NodePtr ein node* ist.

    
Howard Hinnant 19.01.2015 03:22
quelle
1

Es scheint sinnvoll zu sein, bestehende Funktionen zu nutzen. Persönlich würde ich einen Schritt weiter gehen und tatsächlich eine vorhandene Implementierung nutzen, um die Kopie zu machen. Im Allgemeinen scheint ein passendes Copy-and-Swap-Idiom der einfachste Weg zu sein, um eine Kopie zuzuweisen:

%Vor%

Dieser Ansatz macht jedoch ein paar Annahmen:

  1. Der Kopierkonstruktor wird in einer Form implementiert, die [optional] einen Zuweisungsverteiler erhält:

    %Vor%
  2. Es wird angenommen, dass der swap() den Zuordner korrekt auswechselt.

Es lohnt sich, darauf hinzuweisen, dass dieser Ansatz den Vorteil hat, relativ einfach zu sein und eine starke Ausnahmegarantie zu bieten, aber er verwendet neu zugewiesenen Speicher. Wenn der Speicher des LHS-Objekts wiederverwendet wird, kann dies zu einer besseren Leistung führen, z. B. weil der verwendete Speicher bereits nahe bei dem Prozessor ist. Das heißt, für eine erste Implementierung würde ich die Kopie- und Swap-Implementierung verwenden (unter Verwendung eines erweiterten Kopierkonstruktors, wie oben erwähnt) und sie durch eine kompliziertere Implementierung ersetzen, wenn das Profiling anzeigt, dass dies erforderlich ist.

    
Dietmar Kühl 15.12.2014 23:38
quelle

Tags und Links