So übertragen Sie unique_ptrs von einem Set in ein anderes Set

So übertragen Sie unique_ptrs von einem Set in ein anderes Set

Übertragung eines std::unique_ptr zu einem anderen std::unique_ptr ist ganz einfach:

std::unique_ptr<int> p1 = std::make_unique<int>(42);
std::unique_ptr<int> p2;

p2 = std::move(p1); // the contents of p1 have been transferred to p2

Einfach peasy, Zitronensaft.

Was ist nun, wenn diese unique_ptr s leben innerhalb von zwei Sätzen? Es sollte genauso einfach sein, die aus dem ersten Satz auf den zweiten Satz zu übertragen, richtig?

Es stellt sich heraus, dass es nicht einfach, auch nicht peasy und noch weniger zitronensaftig ist. Es sei denn, Sie haben C++17, in diesem Fall ist es ein Kinderspiel. Aber vor C++17 ist es das nicht. Hier sind verschiedene Alternativen, die Sie verwenden können, um dies zu erreichen.

Sehen wir uns zuerst das Motivationsproblem an.

Der Fall:Übertragen von Sätzen von unique_ptrs

Wir beginnen damit, zu sehen, was eine std::set ist von std::unique_ptr darstellen würde, und dann sehen wir, welches Problem auftritt, wenn versucht wird, den Inhalt eines Sets in ein anderes zu übertragen.

Sätze von unique_ptrs:einzigartig und polymorph

Zu Beginn haben Sie sich vielleicht gefragt, warum Sie unique_ptr tun sollten auf einem int wie im obigen Beispiel. Abgesehen davon, ein einfaches Beispiel zu zeigen, hat es überhaupt keinen Nutzen.

Ein realistischerer Fall wäre Laufzeitpolymorphismus durch Vererbung mit einem Base Klasse, die Derived haben kann Klassen:

Und wir würden die Basisklasse polymorph verwenden, indem wir sie mit einer Art Handle (Zeiger oder Referenz) halten. Um die Speicherverwaltung zu kapseln, würden wir einen std::unique_ptr<Base> verwenden .

Wenn wir nun eine Sammlung von mehreren Objekten wollen, die Base implementieren , aber das könnte von jeder abgeleiteten Klasse sein, wir können eine Sammlung von unique_ptr<Base> verwenden s .

Schließlich möchten wir vielleicht verhindern, dass unsere Sammlung Duplikate enthält. Das ist was std::set tut. Beachten Sie, dass zum Implementieren dieser Einschränkung std::set braucht eine Möglichkeit, seine Objekte miteinander zu vergleichen.

In der Tat, indem Sie eine Menge auf diese Weise deklarieren:

std::set<std::unique_ptr<Base>>

der Vergleich zwischen Elementen der Menge ruft operator< auf von std::unique_ptr , die die Speicheradressen der darin enthaltenen Zeiger vergleicht.

In den meisten Fällen ist dies nicht das, was Sie wollen. Wenn wir „keine Duplikate“ denken, bedeutet das im Allgemeinen „keine logischen Duplikate“ wie in:Keine zwei Elemente haben den gleichen Wert. Und nicht „keine zwei Elemente befinden sich an derselben Adresse im Speicher“.

Um keine logischen Duplikate zu implementieren, müssen wir operator< aufrufen auf Base (vorausgesetzt, es existiert, vielleicht mit einer ID, die von Base bereitgestellt wird zum Beispiel) zum Vergleichen von Elementen und bestimmt, ob es sich um Duplikate handelt. Und damit das Set diesen Operator verwendet, müssen wir den Komparator des Sets anpassen:

struct ComparePointee
{
    template<typename T>
    bool operator()(std::unique_ptr<T> const& up1, std::unique_ptr<T> const& up2)
    {
        return *up1 < *up2;
    }
};

std::set<std::unique_ptr<int>, ComparePointee> mySet;

Um zu vermeiden, diesen Typ jedes Mal zu schreiben, wenn wir einen solchen Satz im Code instanziieren, können wir seine technischen Aspekte hinter einem Alias ​​verstecken:

template<typename T>
using UniquePointerSet = std::set<std::unique_ptr<T>, ComparePointee>;

Übertragen von unique_ptrs zwischen zwei Sätzen

OK. Wir sind fertig (ha-ha) und bereit, die Elemente eines Sets auf ein anderes zu übertragen. Hier sind unsere beiden Sets:

UniquePointerSet<Base> source;
source.insert(std::make_unique<Derived>());

UniquePointerSet<Base> destination;

Um Elemente effizient zu übertragen, verwenden wir den insert Methode:

destination.insert(begin(source), end(source));

Dies führt jedoch zu einem Kompilierungsfehler!

error: use of deleted function 'std::unique_ptr<_Tp, _Dp>::unique_ptr(const std::unique_ptr<_Tp, _Dp>&) [with _Tp = Base; _Dp = std::default_delete<Base>]'

In der Tat die insert Methoden versucht, eine Kopie des unique_ptr zu erstellen Elemente.

Was ist dann zu tun?

Neue Methode von C++17 am Set:merge

set s und map s in C++ sind intern als Bäume implementiert. Dadurch können sie die algorithmischen Komplexitäten sicherstellen, die durch die Methoden ihrer Schnittstelle garantiert werden. Vor C++17 wurde es nicht in der Benutzeroberfläche angezeigt.

C++17 fügt den merge hinzu Methode zu Sätzen:

destination.merge(source);

Das macht destination übernehmen die Knoten des Baums innerhalb von source . Es ist wie das Spleißen von Listen. Also nach Ausführung dieser Zeile destination hat die Elemente, die source hatte, und source ist leer.

Und da nur die Knoten geändert werden und nicht der Inhalt, wird die Datei unique_ptr s fühlen nichts. Sie werden nicht einmal bewegt.

destination hat jetzt den unique_ptr s, Ende der Geschichte.

Wenn Sie C++17 nicht in Produktion haben, was bei vielen Leuten der Fall ist, während ich diese Zeilen schreibe, was können Sie tun?

Wir können uns nicht von einem Set bewegen

Der Standardalgorithmus zum Verschieben von Elementen von einer Sammlung in eine andere Sammlung ist std::move . So funktioniert es mit std::vector :

std::vector<std::unique_ptr<Base>> source;
source.push_back(std::make_unique<Derived>());

std::vector<std::unique_ptr<Base>> destination;

std::move(begin(source), end(source), std::back_inserter(destination));

nach Ausführung dieser Zeile destination hat die Elemente, die source hatte und source ist nicht leer, hat aber einen leeren unique_ptr s.

Versuchen wir jetzt dasselbe mit unseren Sets:

UniquePointerSet<Base> source;
source.insert(std::make_unique<Derived>());

UniquePointerSet<Base> destination;

std::move(begin(source), end(source), std::inserter(destination, end(destination)));

Wir erhalten denselben Kompilierungsfehler wie am Anfang, etwa unique_ptr s werden kopiert:

error: use of deleted function 'std::unique_ptr<_Tp, _Dp>::unique_ptr(const std::unique_ptr<_Tp, _Dp>&)

Das mag überraschend erscheinen. Der Zweck des std::move Algorithmus ist es, Kopien auf unique_ptr zu vermeiden Elemente und verschieben Sie sie stattdessen, warum werden sie dann kopiert??

Die Antwort liegt darin, wie die Menge den Zugriff auf ihre Elemente bereitstellt. Beim Dereferenzieren gibt der Iterator eines Sets kein unique_ptr& zurück , sondern ein const unique_ptr& . Es soll sicherstellen, dass die Werte innerhalb des Satzes nicht geändert werden, ohne dass der Satz davon Kenntnis hat. Tatsächlich könnte es seine Invariante der Sortierung brechen.

Folgendes passiert also:

  • std::move dereferenziert den Iterator am Set und erhält einen const unique_ptr& ,
  • ruft std::move auf auf diese Referenzen und erhalten so einen const unique_ptr&& ,
  • er ruft den insert auf -Methode auf dem Insert-Ausgabe-Iterator und übergibt ihm diesen const unique_ptr&& ,
  • der insert -Methode hat zwei Überladungen:eine, die einen const unique_ptr& akzeptiert , und eine, die einen unique_ptr&& akzeptiert . Wegen const In dem Typ, den wir übergeben, kann der Compiler diesen Aufruf der zweiten Methode nicht auflösen und ruft stattdessen die erste auf.

Dann ruft der Insert Output Iterator den insert auf Überlastung des Sets, das const unique_ptr& benötigt und ruft wiederum den Kopierkonstruktor von unique_ptr auf mit dieser L-Wert-Referenz, und das führt zum Kompilierungsfehler.

Ein Opfer bringen

Vor C++17 scheint es also nicht möglich zu sein, Elemente aus einem Set zu verschieben. Irgendwas muss her:entweder Umzug, oder die Sets. Dies führt uns zu zwei möglichen Aspekten, auf die wir verzichten sollten.

Das Set behalten, aber die Kopien bezahlen

Um die Bewegung aufzugeben und zu akzeptieren, die Elemente von einer Menge in eine andere zu kopieren, müssen wir eine Kopie des Inhalts erstellen, auf den der unique_ptr zeigt s.

Nehmen wir dazu an, dass Base hat ist ein polymorpher Klon, der durch seine Methode cloneBase implementiert wurde , überschrieben in Derived :

class Base
{
public:
    virtual std::unique_ptr<Base> cloneBase() const = 0;

    // rest of Base...
};

class Derived : public Base
{
public:
    std::unique_ptr<Base> cloneBase() const override
    {
        return std::make_unique<Derived>(*this);
    }

    // rest of Derived...
};

Auf der Call-Site können wir Kopien des unique_ptr erstellen s von einem Satz zum anderen, zum Beispiel auf diese Weise:

auto clone = [](std::unique_ptr<Base> const& pointer){ return pointer->cloneBase(); };
std::transform(begin(source), end(source), std::inserter(destination, end(destination)), clone);

Oder mit einer for-Schleife:

for (auto const& pointer : source)
{
    destination.insert(pointer->cloneBase());
}

In Bewegung bleiben und das Set wegwerfen

Das Set, das die Bewegung nicht zulässt, ist das source einstellen. Wenn Sie nur den destination benötigen Um eindeutige Elemente zu haben, können Sie den source ersetzen gesetzt durch einen std::vector .

Tatsächlich std::vector fügt kein const hinzu auf den von seinem Iterator zurückgegebenen Wert. Wir können daher seine Elemente mit std::move daraus verschieben Algorithmus:

std::vector<std::unique_ptr<Base>> source;
source.push_back(std::make_unique<Derived>(42));

std::set<std::unique_ptr<Base>> destination;

std::move(begin(source), end(source), std::inserter(destination, end(destination)));

Dann die destination Satz enthält einen unique_ptr die den Inhalt hat, der früher in dem der source war , und die source Vektor enthält jetzt einen leeren unique_ptr .

Im Kopf leben

Sie sehen, dass es Möglichkeiten gibt, das Problem der Übertragung von unique_ptr zu umgehen s von einem Satz zu einem anderen. Aber die wirkliche Lösung ist der merge Methode von std::set in C++17.

Die Standardbibliothek wird mit der Weiterentwicklung der Sprache immer besser. Lassen Sie uns tun, was wir können, um auf die neueste Version von C++ umzusteigen (ha-ha) und niemals zurückblicken.

Verwandte Artikel:

  • Move-Iteratoren:Wo die STL auf Move-Semantik trifft
  • Kluge Entwickler verwenden intelligente Zeiger
  • Die STL-Lernressource