Dlaczego nie ma domyślnego przypisania do przenoszenia / konstruktora przenoszenia?

89

Jestem prostym programistą. Zmienne składowe mojej klasy najczęściej składają się z typów POD i kontenerów STL. Z tego powodu rzadko muszę pisać operatory przypisania lub konstruktory kopiujące, ponieważ są one implementowane domyślnie.

Dodajmy do tego, że jeśli używam std::movena obiektach, które nie są ruchome, używa operatora przypisania, co oznacza, że std::movejest całkowicie bezpieczny.

Ponieważ jestem prostym programistą, chciałbym skorzystać z możliwości przenoszenia bez dodawania konstruktora przenoszenia / operatora przypisania do każdej klasy, którą piszę, ponieważ kompilator mógłby po prostu zaimplementować je jako „ this->member1_ = std::move(other.member1_);...

Ale tak nie jest (przynajmniej nie w Visual 2010), czy jest jakiś szczególny powód?

Co ważniejsze; czy jest jakiś sposób, aby to obejść?

Aktualizacja: Jeśli spojrzysz na odpowiedź GManNickG, zapewni on świetne makro do tego. A jeśli nie wiesz, jeśli zaimplementujesz semantykę ruchu, możesz usunąć funkcję elementu członkowskiego wymiany.

Viktor Sehr
źródło
5
wiesz, że możesz kazać kompilatorowi wygenerować domyślny program do przenoszenia
aaronman
3
std :: move nie wykonuje ruchu, po prostu rzuca z wartości l na wartość r. Ruch jest nadal wykonywany przez konstruktora przenoszenia.
Owen Delahoy
1
O czym mówisz MyClass::MyClass(Myclass &&) = default;?
Sandburg
Tak, obecnie :)
Viktor Sehr

Odpowiedzi:

76

Niejawne generowanie konstruktorów przenoszenia i operatorów przypisania było sporne, a ostatnie wersje C ++ standardu zostały poddane poważnym zmianom, więc obecnie dostępne kompilatory będą prawdopodobnie zachowywać się inaczej w odniesieniu do niejawnego generowania.

Aby uzyskać więcej informacji na temat historii wydania, zapoznaj się z listą artykułów WG21 z 2010 r. I wyszukaj „mov”

Aktualna specyfikacja (N3225, od listopada) podaje (N3225 12,8 / 8):

Jeśli definicja klasy Xnie deklaruje jawnie konstruktora przenoszenia, zostanie on niejawnie zadeklarowany jako domyślny wtedy i tylko wtedy, gdy

  • X nie ma konstruktora kopiującego zadeklarowanego przez użytkownika, a

  • X nie ma operatora przypisania kopii zadeklarowanego przez użytkownika,

  • X nie ma operatora przypisania przeniesienia zadeklarowanego przez użytkownika,

  • X nie ma destruktora zadeklarowanego przez użytkownika, a

  • konstruktor przenoszenia nie zostałby niejawnie zdefiniowany jako usunięty.

Jest podobny język w 12.8 / 22 określający, kiedy operator przypisania przeniesienia jest niejawnie zadeklarowany jako domyślny. Pełną listę zmian wprowadzonych w celu obsługi bieżącej specyfikacji generowania niejawnych ruchów można znaleźć w N3203: Zaostrzanie warunków generowania niejawnych ruchów , która była w dużej mierze oparta na jednej z rezolucji zaproponowanych w artykule Bjarne Stroustrupa N3201: Moving right along .

James McNellis
źródło
4
Napisałem mały artykuł z kilkoma diagramami opisującymi relacje dla niejawnego (przeniesienia) konstruktora / przypisania tutaj: mmocny.wordpress.com/2010/12/09/implicit-move-wont-go
mmocny
Ugh, więc ilekroć muszę zdefiniować puste destruktory w polimorficznych klasach bazowych tylko po to, aby określić je jako wirtualne, muszę również jawnie zdefiniować konstruktor ruchu i operator przypisania :(.
someguy
@James McNellis: Próbowałem tego wcześniej, ale kompilatorowi się to nie podobało. Miałem zamieścić komunikat o błędzie w tej właśnie odpowiedzi, ale po próbie odtworzenia błędu zdałem sobie sprawę, że o nim wspomina cannot be defaulted *in the class body*. Tak więc zdefiniowałem destruktor na zewnątrz i zadziałało :). Jednak wydaje mi się to trochę dziwne. Czy ktoś ma wyjaśnienie? Kompilator to gcc 4.6.1
someguy
3
Może moglibyśmy uzyskać aktualizację tej odpowiedzi teraz, gdy C ++ 11 jest ratyfikowany? Ciekawe, jakie zachowania wygrały.
Joseph Garvin
2
@Guy Avraham: Myślę, że to, co mówiłem (minęło 7 lat), to to, że jeśli mam zadeklarowany przez użytkownika destruktor (nawet pusty wirtualny), żaden konstruktor ruchu nie zostanie domyślnie zadeklarowany jako domyślny. Przypuszczam, że spowodowałoby to semantykę kopiowania? (Od lat nie dotykałem C ++.) James McNellis skomentował następnie, że virtual ~D() = default;powinno to działać i nadal umożliwiać niejawny konstruktor ruchu.
someguy
13

Niejawnie wygenerowane konstruktory przenoszenia zostały uwzględnione w standardzie, ale mogą być niebezpieczne. Zobacz analizę Dave'a Abrahamsa .

Ostatecznie jednak standard zawiera niejawne generowanie konstruktorów przenoszenia i operatorów przypisania przenoszenia, chociaż z dość dużą listą ograniczeń:

Jeśli definicja klasy X nie deklaruje jawnie konstruktora przenoszenia, zostanie on niejawnie zadeklarowany jako domyślny wtedy i tylko wtedy, gdy
- X nie ma konstruktora kopiującego zadeklarowanego przez użytkownika,
- X nie ma zadeklarowanego przez użytkownika operatora przypisania kopiowania ,
- X nie ma operatora przypisania przenoszenia zadeklarowanego przez użytkownika,
- X nie ma destruktora zadeklarowanego przez użytkownika, oraz
- konstruktor przenoszenia nie zostałby domyślnie zdefiniowany jako usunięty.

Jednak to nie wszystko w tej historii. Ctor można zadeklarować, ale nadal zdefiniować jako usunięty

Niejawnie zadeklarowany konstruktor kopiowania / przenoszenia jest wbudowanym publicznym składnikiem swojej klasy. Domyślny konstruktor kopiowania / przenoszenia dla klasy X jest zdefiniowany jako usunięty (8.4.3), jeśli X ma:

- element członkowski wariantu z nietrywialnym odpowiednim konstruktorem, a X jest klasą podobną do unii,
- niestatyczny element danych klasy M (lub jego tablicy), którego nie można skopiować / przenieść z powodu rozwiązania przeciążenia (13.3), jak zastosowane do odpowiedniego konstruktora M, powoduje niejednoznaczność lub funkcję, która jest usunięta lub niedostępna z domyślnego konstruktora,
- bezpośrednia lub wirtualna klasa bazowa B, której nie można skopiować / przenieść z powodu rozwiązania przeciążenia (13.3), jak zastosowano do odpowiedniego konstruktora B powoduje niejednoznaczność lub funkcję, która jest usunięta lub niedostępna z domyślnego konstruktora,
- dowolna bezpośrednia lub wirtualna klasa bazowa lub niestatyczna klasa danych typu z destruktorem, który jest usunięty lub niedostępny z domyślnego konstruktora,
- w przypadku konstruktora kopiującego, niestatycznej składowej danych typu referencyjnego rvalue, lub
- w przypadku konstruktora przenoszenia, niestatycznej składowej danych lub bezpośredniej lub wirtualnej klasy bazowej o typie, który nie ma konstruktora przenoszenia i nie jest trywialny do kopiowania.

Jerry Coffin
źródło
Obecny projekt roboczy zezwala na niejawne generowanie ruchu w pewnych warunkach i myślę, że rezolucja w dużej mierze odnosi się do obaw Abrahamsa.
James McNellis
Nie jestem pewien, czy rozumiem, jaki ruch może się zepsuć w przykładzie między Tweak 2 i Tweak 3. Czy możesz to wyjaśnić?
Matthieu M.
@Matthieu M .: Zarówno Tweak 2, jak i Tweak 3 są zepsute i na bardzo podobny sposób. W Tweaku 2 istnieją prywatne elementy członkowskie z niezmiennikami, które mogą zostać złamane przez obiekt ruchu. W Tweaku 3 klasa nie ma samych członków prywatnych , ale ponieważ korzysta z dziedziczenia prywatnego, publiczni i chronieni członkowie bazy stają się prywatnymi członkami elementu pochodnego, co prowadzi do tego samego problemu.
Jerry Coffin
1
Naprawdę nie rozumiałem, jak konstruktor przenoszenia mógłby złamać niezmiennik klasy Tweak2. Przypuszczam, że ma to coś wspólnego z faktem, że Numberzostałby przeniesiony, a vectorskopiowany ... ale nie jestem pewien: / Rozumiem, że problem spadłby kaskadowo Tweak3.
Matthieu M.
Link, który podałeś, wydaje się być martwy?
Wolf
8

(na razie pracuję nad głupim makrem ...)

Tak, ja też poszedłem tą drogą. Oto twoje 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

Wcześniejsze

// 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

(Usunąłem prawdziwe komentarze, które są obszerne i dokumentalne.)

Określasz bazy i / lub składowe w swojej klasie jako listę preprocesorów, na przykład:

#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;
};

Wychodzi konstruktor ruchu i operator przypisania ruchu.

(Na marginesie, jeśli ktoś wie, jak mogę połączyć szczegóły w jedno makro, byłoby to świetne.)

GManNickG
źródło
Dziękuję bardzo, mój jest dość podobny, z wyjątkiem tego, że musiałem podać liczbę zmiennych składowych jako argument (co naprawdę jest do bani).
Viktor Sehr
1
@Viktor: Nie ma problemu. Jeśli nie jest za późno, myślę, że jedną z pozostałych odpowiedzi należy oznaczyć jako zaakceptowaną. Mój był bardziej „przy okazji, oto sposób”, a nie odpowiedzią na twoje prawdziwe pytanie.
GManNickG
1
Jeśli poprawnie czytam twoje makro, to gdy tylko Twój kompilator zaimplementuje domyślne elementy przenoszenia, powyższe przykłady staną się niemożliwe do skopiowania. Niejawne generowanie składowych kopii jest zabronione, gdy istnieją jawnie zadeklarowane elementy przenoszenia.
Howard Hinnant
@Howard: W porządku, do tego czasu jest to tymczasowe rozwiązanie. :)
GManNickG
GMan: To makro dodaje moveconstructor \ assign, jeśli masz funkcję zamiany:
Viktor Sehr,
4

VS2010 nie robi tego, ponieważ nie były standardowe w momencie wdrożenia.

Szczeniak
źródło