Warum kein Standard-Verschiebungszuweisungs- / Verschiebungskonstruktor?

89

Ich bin ein einfacher Programmierer. Die Variablen meiner Klassenmitglieder bestehen meistens aus POD-Typen und STL-Containern. Aus diesem Grund muss ich selten Zuweisungsoperatoren oder Kopierkonstruktoren schreiben, da diese standardmäßig implementiert sind.

Wenn ich std::moveObjekte verwende, die nicht beweglich sind, wird der Zuweisungsoperator verwendet, was bedeutet, dass dies std::moveabsolut sicher ist.

Da ich ein einfacher Programmierer bin, möchte ich die Verschiebungsfunktionen nutzen, ohne jeder Klasse, die ich schreibe, einen Verschiebungskonstruktor / Zuweisungsoperator hinzuzufügen, da der Compiler sie einfach als " this->member1_ = std::move(other.member1_);..." implementieren könnte.

Aber gibt es keinen besonderen Grund dafür (zumindest nicht in Visual 2010)?

Wichtiger; Gibt es eine Möglichkeit, dies zu umgehen?

Update: Wenn Sie sich die Antwort von GManNickG ansehen, bietet er ein großartiges Makro dafür. Und wenn Sie nicht wussten, dass Sie die Swap-Member-Funktion entfernen können, wenn Sie die Verschiebungssemantik implementieren.

Viktor Sehr
quelle
5
Sie wissen, dass Sie den Compiler einen Standardzug ctor generieren lassen können
aaronman
3
std :: move führt keine Verschiebung durch, sondern wandelt einfach von einem l-Wert in einen r-Wert um. Die Verschiebung wird weiterhin vom Verschiebungskonstruktor ausgeführt.
Owen Delahoy
1
Sprechen Sie über MyClass::MyClass(Myclass &&) = default;?
Sandburg
Ja, heutzutage :)
Viktor Sehr

Antworten:

76

Die implizite Generierung von Verschiebungskonstruktoren und Zuweisungsoperatoren war umstritten, und die jüngsten Entwürfe des C ++ - Standards wurden grundlegend überarbeitet, sodass sich derzeit verfügbare Compiler in Bezug auf die implizite Generierung wahrscheinlich anders verhalten.

Weitere Informationen zur Geschichte der Ausgabe finden Sie in der WG21-Papierliste 2010 und suchen Sie nach "mov".

In der aktuellen Spezifikation (N3225, ab November) heißt es (N3225 12.8 / 8):

Wenn die Definition einer Klasse Xeinen Verschiebungskonstruktor nicht explizit deklariert, wird einer implizit genau dann als voreingestellt deklariert, wenn

  • X hat keinen vom Benutzer deklarierten Kopierkonstruktor und

  • X hat keinen vom Benutzer deklarierten Kopierzuweisungsoperator,

  • X hat keinen vom Benutzer deklarierten Verschiebungszuweisungsoperator,

  • X hat keinen vom Benutzer deklarierten Destruktor und

  • Der Verschiebungskonstruktor würde nicht implizit als gelöscht definiert.

In 12.8 / 22 gibt es eine ähnliche Sprache, die angibt, wann der Verschiebungszuweisungsoperator implizit als Standard deklariert wird. Die vollständige Liste der Änderungen, die zur Unterstützung der aktuellen Spezifikation der impliziten Bewegungsgenerierung in N3203 vorgenommen wurden, finden Sie in N3203: Verschärfung der Bedingungen für die Generierung impliziter Bewegungen , die weitgehend auf einer der in Bjarne Stroustrups Artikel N3201 vorgeschlagenen Schritte beruhten: Weitergehen .

James McNellis
quelle
4
Ich habe hier einen kleinen Artikel mit einigen Diagrammen geschrieben, in denen die Beziehungen für den impliziten (Verschiebungs-) Konstruktor / die implizite Zuweisung beschrieben werden: mmocny.wordpress.com/2010/12/09/implicit-move-wont-go
mmocny
Ugh also, wann immer ich leere Destruktoren in polymorphen Basisklassen definieren muss, nur um sie als virtuell anzugeben, muss ich auch den Verschiebungskonstruktor und den Zuweisungsoperator explizit definieren :(.
irgendwann
@ James McNellis: Das habe ich vorher versucht, aber der Compiler schien es nicht zu mögen. Ich wollte die Fehlermeldung in dieser Antwort posten, aber nachdem ich versucht hatte, den Fehler zu reproduzieren, stellte ich fest, dass er dies erwähnt cannot be defaulted *in the class body*. Also habe ich den Destruktor draußen definiert und es hat funktioniert :). Ich finde es allerdings etwas seltsam. Hat jemand eine Erklärung? Compiler ist gcc 4.6.1
irgendwann
3
Vielleicht könnten wir jetzt, da C ++ 11 ratifiziert ist, ein Update auf diese Antwort bekommen? Neugierig, welche Verhaltensweisen gewonnen haben.
Joseph Garvin
2
@ Guy Avraham: Ich denke, was ich gesagt habe (es sind 7 Jahre vergangen), ist, dass wenn ich einen vom Benutzer deklarierten Destruktor habe (sogar einen leeren virtuellen), kein Verschiebungskonstruktor implizit als Standard deklariert wird. Ich nehme an, das würde zu einer Kopiersemantik führen? (Ich habe C ++ seit Jahren nicht mehr berührt.) James McNellis kommentierte dann, dass dies virtual ~D() = default;funktionieren und dennoch einen impliziten Verschiebungskonstruktor zulassen sollte.
Irgendwann
13

Implizit generierte Verschiebungskonstruktoren wurden für den Standard berücksichtigt, können jedoch gefährlich sein. Siehe Dave Abrahams Analyse .

Am Ende enthielt der Standard jedoch die implizite Generierung von Verschiebungskonstruktoren und Verschiebungszuweisungsoperatoren, allerdings mit einer ziemlich umfangreichen Liste von Einschränkungen:

Wenn die Definition einer Klasse X einen Verschiebungskonstruktor nicht explizit deklariert, wird einer implizit genau dann als voreingestellt deklariert, wenn
- X keinen vom Benutzer deklarierten Kopierkonstruktor hat,
- X keinen vom Benutzer deklarierten Kopierzuweisungsoperator hat ,
- X hat keinen vom Benutzer deklarierten Verschiebungszuweisungsoperator,
- X hat keinen vom Benutzer deklarierten Destruktor, und
- der Verschiebungskonstruktor würde nicht implizit als gelöscht definiert.

Das ist aber noch nicht alles, was die Geschichte zu bieten hat. Ein ctor kann deklariert, aber dennoch als gelöscht definiert werden:

Ein implizit deklarierter Copy / Move-Konstruktor ist ein öffentliches Inline-Mitglied seiner Klasse. Ein standardmäßiger Kopier- / Verschiebungskonstruktor für eine Klasse X wird als gelöscht (8.4.3) definiert, wenn X Folgendes hat:

- Ein Variantenelement mit einem nicht trivialen entsprechenden Konstruktor und X ist eine unionähnliche Klasse.
- Ein nicht statisches Datenelement des Klassentyps M (oder eines Arrays davon), das aufgrund der Überlastungsauflösung (13.3) nicht kopiert / verschoben werden kann Wird auf den entsprechenden Konstruktor von M angewendet, führt dies zu einer Mehrdeutigkeit oder einer Funktion, die vom Standardkonstruktor gelöscht wird oder auf die nicht zugegriffen werden kann. Dies ist
eine direkte oder virtuelle Basisklasse B, die aufgrund der Überlastungsauflösung (13.3) nicht kopiert / verschoben werden kann, wie sie auf den entsprechenden Konstruktor von B angewendet wird führt zu einer Mehrdeutigkeit oder einer Funktion, auf die vom Standardkonstruktor gelöscht wird oder auf die nicht zugegriffen werden kann.
- Jede direkte oder virtuelle Basisklasse oder jedes nicht statische Datenelement eines Typs mit einem Destruktor, auf den vom Standardkonstruktor gelöscht wird oder auf den nicht zugegriffen werden kann.
- für den Kopierkonstruktor ein nicht statisches Datenelement vom Referenztyp rvalue oder
- für den Verschiebungskonstruktor ein nicht statisches Datenelement oder eine direkte oder virtuelle Basisklasse mit einem Typ, der keinen Verschiebungskonstruktor hat und nicht trivial ist kopierbar.

Jerry Sarg
quelle
Der aktuelle Arbeitsentwurf erlaubt unter bestimmten Bedingungen die implizite Erzeugung von Bewegungen, und ich denke, die Entschließung geht weitgehend auf Abrahams Bedenken ein.
James McNellis
Ich bin nicht sicher, ob ich verstanden habe, welche Bewegung im Beispiel zwischen Tweak 2 und Tweak 3 unterbrochen werden kann. Können Sie das erklären?
Matthieu M.
@Matthieu M.: Sowohl Tweak 2 als auch Tweak 3 sind kaputt und auf ziemlich ähnliche Weise wirklich. In Tweak 2 gibt es private Mitglieder mit Invarianten, die durch den Verschiebungsfaktor gebrochen werden können. In Tweak 3, wird die Klasse nicht private Mitglieder selbst , aber da nutzt es private Vererbung, die öffentlichen und geschützte Elemente der Basis private Mitglieder des abgeleitet werden, zu dem gleichen Problem führen.
Jerry Coffin
1
Ich habe nicht wirklich verstanden, wie der Verschiebungskonstruktor die Klasseninvariante einbrechen würde Tweak2. Ich nehme an, es hat etwas damit zu tun, dass das Numberverschoben und vectorkopiert wird ... aber ich bin mir nicht sicher: / Ich verstehe, dass das Problem kaskadieren würde Tweak3.
Matthieu M.
Der Link, den Sie gegeben haben, scheint tot zu sein?
Wolf
8

(Im Moment arbeite ich an einem dummen Makro ...)

Ja, ich bin auch diesen Weg gegangen. Hier ist dein Makro:

// detail/move_default.hpp
#ifndef UTILITY_DETAIL_MOVE_DEFAULT_HPP
#define UTILITY_DETAIL_MOVE_DEFAULT_HPP

#include <boost/preprocessor.hpp>

#define UTILITY_MOVE_DEFAULT_DETAIL_CONSTRUCTOR_BASE(pR, pData, pBase) pBase(std::move(pOther))
#define UTILITY_MOVE_DEFAULT_DETAIL_ASSIGNMENT_BASE(pR, pData, pBase) pBase::operator=(std::move(pOther));

#define UTILITY_MOVE_DEFAULT_DETAIL_CONSTRUCTOR(pR, pData, pMember) pMember(std::move(pOther.pMember))
#define UTILITY_MOVE_DEFAULT_DETAIL_ASSIGNMENT(pR, pData, pMember) pMember = std::move(pOther.pMember);

#define UTILITY_MOVE_DEFAULT_DETAIL(pT, pBases, pMembers)                                               \
        pT(pT&& pOther) :                                                                               \
        BOOST_PP_SEQ_ENUM(BOOST_PP_SEQ_TRANSFORM(                                                       \
            UTILITY_MOVE_DEFAULT_DETAIL_CONSTRUCTOR_BASE, BOOST_PP_EMPTY, pBases))                      \
        ,                                                                                               \
        BOOST_PP_SEQ_ENUM(BOOST_PP_SEQ_TRANSFORM(                                                       \
            UTILITY_MOVE_DEFAULT_DETAIL_CONSTRUCTOR, BOOST_PP_EMPTY, pMembers))                         \
        {}                                                                                              \
                                                                                                        \
        pT& operator=(pT&& pOther)                                                                      \
        {                                                                                               \
            BOOST_PP_SEQ_FOR_EACH(UTILITY_MOVE_DEFAULT_DETAIL_ASSIGNMENT_BASE, BOOST_PP_EMPTY, pBases)  \
            BOOST_PP_SEQ_FOR_EACH(UTILITY_MOVE_DEFAULT_DETAIL_ASSIGNMENT, BOOST_PP_EMPTY, pMembers)     \
                                                                                                        \
            return *this;                                                                               \
        }

#define UTILITY_MOVE_DEFAULT_BASES_DETAIL(pT, pBases)                                                   \
        pT(pT&& pOther) :                                                                               \
        BOOST_PP_SEQ_ENUM(BOOST_PP_SEQ_TRANSFORM(                                                       \
            UTILITY_MOVE_DEFAULT_DETAIL_CONSTRUCTOR_BASE, BOOST_PP_EMPTY, pBases))                      \
        {}                                                                                              \
                                                                                                        \
        pT& operator=(pT&& pOther)                                                                      \
        {                                                                                               \
            BOOST_PP_SEQ_FOR_EACH(UTILITY_MOVE_DEFAULT_DETAIL_ASSIGNMENT_BASE, BOOST_PP_EMPTY, pBases)  \
                                                                                                        \
            return *this;                                                                               \
        }

#define UTILITY_MOVE_DEFAULT_MEMBERS_DETAIL(pT, pMembers)                                               \
        pT(pT&& pOther) :                                                                               \
        BOOST_PP_SEQ_ENUM(BOOST_PP_SEQ_TRANSFORM(                                                       \
            UTILITY_MOVE_DEFAULT_DETAIL_CONSTRUCTOR, BOOST_PP_EMPTY, pMembers))                         \
        {}                                                                                              \
                                                                                                        \
        pT& operator=(pT&& pOther)                                                                      \
        {                                                                                               \
            BOOST_PP_SEQ_FOR_EACH(UTILITY_MOVE_DEFAULT_DETAIL_ASSIGNMENT, BOOST_PP_EMPTY, pMembers)     \
                                                                                                        \
            return *this;                                                                               \
        }

#endif

Wenn Sie

// move_default.hpp
#ifndef UTILITY_MOVE_DEFAULT_HPP
#define UTILITY_MOVE_DEFAULT_HPP

#include "utility/detail/move_default.hpp"

// move bases and members
#define UTILITY_MOVE_DEFAULT(pT, pBases, pMembers) UTILITY_MOVE_DEFAULT_DETAIL(pT, pBases, pMembers)

// base only version
#define UTILITY_MOVE_DEFAULT_BASES(pT, pBases) UTILITY_MOVE_DEFAULT_BASES_DETAIL(pT, pBases)

// member only version
#define UTILITY_MOVE_DEFAULT_MEMBERS(pT, pMembers) UTILITY_MOVE_DEFAULT_MEMBERS_DETAIL(pT, pMembers)

#endif

(Ich habe die echten Kommentare entfernt, die Länge und Dokumentation sind.)

Sie geben die Basen und / oder Mitglieder in Ihrer Klasse als Präprozessorliste an, zum Beispiel:

#include "move_default.hpp"

struct foo
{
    UTILITY_MOVE_DEFAULT_MEMBERS(foo, (x)(str));

    int x;
    std::string str;
};

struct bar : foo, baz
{
    UTILITY_MOVE_DEFAULT_BASES(bar, (foo)(baz));
};

struct baz : bar
{
    UTILITY_MOVE_DEFAULT(baz, (bar), (ptr));

    void* ptr;
};

Und es kommt ein Verschiebungskonstruktor und ein Verschiebungszuweisungsoperator heraus.

(Abgesehen davon, wenn jemand weiß, wie ich die Details in einem Makro kombinieren könnte, wäre das sehr gut.)

GManNickG
quelle
Vielen Dank, meine ist ziemlich ähnlich, außer dass ich die Anzahl der Mitgliedsvariablen als Argument übergeben musste (was wirklich scheiße ist).
Viktor Sehr
1
@ Victor: Kein Problem. Wenn es nicht zu spät ist, sollten Sie eine der anderen Antworten als akzeptiert markieren. Meins war eher ein "Übrigens, hier ist ein Weg" und keine Antwort auf Ihre eigentliche Frage.
GManNickG
1
Wenn ich Ihr Makro richtig lese, werden Ihre obigen Beispiele nicht mehr kopierbar, sobald Ihr Compiler standardmäßige Verschiebungselemente implementiert. Die implizite Generierung von Kopierelementen wird verhindert, wenn explizit deklarierte Verschiebungselemente vorhanden sind.
Howard Hinnant
@ Howard: Das ist okay, es ist bis dahin eine vorübergehende Lösung. :)
GManNickG
GMan: Dieses Makro fügt moveconstructor \ assign hinzu, wenn Sie eine Swap-Funktion haben:
Viktor Sehr
4

VS2010 tut dies nicht, da sie zum Zeitpunkt der Implementierung nicht Standard waren.

Hündchen
quelle