Unique Ptr und Copy and Swap Idiom



  • Hallo zusammen,

    meine Ausgangslage ist, dass ich eine Klasse habe, die mit einem Unique Ptr ein Pimpl implementiert. Also, schematisch so was:

    class Foo {
    public:
    //irgendwas
    private:
      struct Impl;
      std::unique_ptr<Impl> pImpl;
    }
    

    Move Constructor und Move Assignment bekomme ich damit frei Haus, die Datentypen im Impl sind alle Kopier und Move fähig.
    Jetzt brauche ich aber doch auch den Copy Konstruktor.

    Naiv hatte ich an sowas gedacht:

    Foo& Foo(const Foo& other):
    pImpl(std::make_unique<Foo::Impl>(*other.pImpl)){}
    
    

    Ähnliches für Copy Assignment:

    Foo& Foo::operator=(const Foo& other) {
        if (this != &other) {
            pImpl.reset(std::make_unique<Foo::Impl>(*other.pImpl));
        }
        return *this;
    }
    

    Jetzt habe ich den Fehler gemacht und angefangen nachzudenken. Es gibt ja noch das Copy & Swap Idiom, welches vereinfacht gesagt, besagt: implementiere den Copy Konstruktor und eine swap Funktion und implementiere damit den Copy Assignment Operator. Damit kann man auch move assignment und move constructor erschlagen.

    Allerdings sehe ich, solange ich keine eigene Speicherverwalteung nutze, keinen Vorteil von Copy & Swap. Übersehe ich da was? Wenn ja, was?

    Vlt sogar allgemeiner: Ist Copy & Swap überhaupt noch aktuell, wenn man konsequent auf Standard Container und Smart Pointer setzt?



  • Copy & Swap ist hauptsächlich gut für zwei Sachen:

    1. In Klassen die selbst RAII implementieren spart man sich damit fummeligen Code im Assignment-Operator
    2. Man kann damit einfach(er) die strong ("all or nothing") Exception-Safety für den Assignment-Operator bekommen - alles was man dafür braucht ist eine entsprechend Exception-sichere swap Funktion

    In deinem konkreten Fall...

    Was Foo angeht fällt (1) schonmal weg. Was (2) angeht: Copy & Swap für Foo ist natürlich ein super-einfacher und super-sicherer Weg den Assignment-Operator strong exception safe zu machen. Der Nachteil ist aber: schlechtere Performance. Du erzwingst damit ja dass immer ein neues Foo::Impl Objekt erzeugt wird und verhinderst bzw. besser gesagt umgehst damit alle eventuellen Optimierungen im Assignment-Operator von Foo::Impl.

    Was Foo::Impl angeht sollte (1) auch kein Thema sein. Klassen die RAII machen sollten immer nur RAII machen und von nur einer Resource. Eine Beschreibung die vermutlich nicht auf Foo::Impl zutrifft. Ob (2) für Foo::Impl wichtig ist kann ich nicht wissen. Kann sein ja kann sein nein.

    Und zu guter letzt: Überleg dir ob "movable" und "copyable" Konzepte sind die für dein Foo überhaupt sinnvoll sind. Nicht alles was theoretisch copyable sein kann sollte auch copyable sein.



  • ps

    @Schlangenmensch sagte in Unique Ptr und Copy and Swap Idiom:

    Ist Copy & Swap überhaupt noch aktuell, wenn man konsequent auf Standard Container und Smart Pointer setzt?

    Das aktuelle "Ideal" ist die Rule of 0: Klassen die keine der "big 5" Funktionen implementieren, weil sie es nicht müssen. Dabei wird Exception-Safety leider gern übersehen, aber das ist dann wieder ein anderes Thema.



  • Danke für deine ausführliche Antwort.

    Strong Exception-Safety ist im Moment keine Anforderung und RAII implementiert die Klasse auch nicht.

    Vlt könnte ich über Umwege auf Kopierbarkeit verzichten, aber eigentlich ist es für die Klasse aber sinnvoll. Ich weiß, so lange ich nicht schreibe wo drum es geht, kann das außer mir keiner Beurteilen...
    Ohne Pimpl hätte ich das ganze Problem auch nicht, dann müsste ich keinen der "big 5" selbst implementieren.

    Ich habe grade eine "smart pimpl" Implementation gefunden: http://oliora.github.io/2015/12/29/pimpl-and-rule-of-zero.html#movable-and-copyable-pimpl
    Ich muss mich da nochmal mehr eindenken, im Moment glaube ich nicht, dass sich der Aufwand in meinem Fall dafür lohnt.



  • @hustbaer sagte in Unique Ptr und Copy and Swap Idiom:

    Dabei wird Exception-Safety leider gern übersehen

    Warum eigentlich? Leuchtet mir grad nicht ein.



  • @Mechanics

    struct MyConfig {
        std::string value1;
        std::string value2;
        std::string value3;
    };
    
    void MyClass::updateConfig() {
         MyConfig newCfg;
         newCfg.value1 = ...;
         newCfg.value2 = ...;
         newCfg.value3 = ...;
         m_config = cfg;
    }
    
    

    Wenn value1, value2 und value3 jetzt zusammenpassen müssen damit die Sache funktionieren kann, dann hast du mit dem Code in der Form ein potentielles Problem. Klar, es ist in diesem Fall schnell mit nem std::move behoben. Ist aber nicht immer so einfach zu sehen und auch nicht immer so einfach zu beheben.

    Und was "einfach zu sehen" angeht: ich kenne genug Leute denen sowas garantiert nicht auffällt bzw. die einfach überhaupt nicht in solchen Bahnen denken. Also Leute die nicht sofort nach dem 1. Satz des Erklärungsversuchs sagen "ups, ja, Mist", sondern denen man wirklich und wahrhaftig erklären muss was hier schief gehen kann.



  • Ok, ist eigentlich logisch. Muss ja nicht mal die basic exception safety gegeben sein, je nach Reihenfolge der Member.



  • @Mechanics sagte in Unique Ptr und Copy and Swap Idiom:

    Muss ja nicht mal die basic exception safety gegeben sein, je nach Reihenfolge der Member.

    Genau. Sobald man

    • Member hat die zusammenpassen müssen und
    • Member hat die nicht no-throw-assignable sind

    ist nicht mehr automatisch sichergestellt dass man noch irgend eine Form der Exception-Safety hat. Was auch einer der Gründe ist warum ich relativ freizügig mit "non-copyable-machen" bin.


Log in to reply