Dlaczego możemy użyć „std :: move” na obiekcie „const”?


113

W C ++ 11 możemy napisać taki kod:

struct Cat {
   Cat(){}
};

const Cat cat;
std::move(cat); //this is valid in C++11

kiedy dzwonię std::move, to znaczy, że chcę przesunąć obiekt, czyli zmienię obiekt. Przesuwanie constobiektu jest nierozsądne, więc dlaczego std::movenie ogranicza tego zachowania? To będzie pułapka w przyszłości, prawda?

Tutaj pułapka oznacza, jak wspomniał Brandon w komentarzu:

"Myślę, że ma na myśli to, że" więzi "go podstępnie podstępnie, ponieważ jeśli nie zdaje sobie sprawy, kończy się z kopią, która nie jest tym, co zamierzał."

W książce „Effective Modern C ++” Scotta Meyersa podaje przykład:

class Annotation {
public:
    explicit Annotation(const std::string text)
     : value(std::move(text)) //here we want to call string(string&&),
                              //but because text is const, 
                              //the return type of std::move(text) is const std::string&&
                              //so we actually called string(const string&)
                              //it is a bug which is very hard to find out
private:
    std::string value;
};

Gdyby std::movezabroniono nam operowania na constobiekcie, moglibyśmy łatwo znaleźć błąd, prawda?


2
Ale spróbuj go przesunąć. Spróbuj zmienić jego stan. std::movesam w sobie nie robi nic na przedmiot. Można by argumentować, że std::movejest źle nazwany.
juanchopanza

3
W rzeczywistości niczego nie rusza . Wszystko, co robi, jest rzutowane na odniesienie do wartości r. try CAT cat2 = std::move(cat);, zakładając, że CATobsługuje regularne przypisywanie ruchów.
WhozCraig

11
std::moveto tylko obsada, tak naprawdę niczego nie rusza
Red Alert

2
@WhozCraig: Ostrożnie, ponieważ kod, który opublikowałeś, kompiluje się i wykonuje bez ostrzeżenia, przez co wprowadza w błąd.
Mooing Duck

1
@MooingDuck Nigdy nie powiedział, że się nie kompiluje. działa tylko dlatego, że domyślny program kopiujący jest włączony. Zgaś to, a koła odpadną.
WhozCraig

Odpowiedzi:


55
struct strange {
  mutable size_t count = 0;
  strange( strange const&& o ):count(o.count) { o.count = 0; }
};

const strange s;
strange s2 = std::move(s);

tutaj widzimy użycie std::movena T const. Zwraca a T const&&. Mamy konstruktora przenoszenia, strangektóry przyjmuje dokładnie ten typ.

I to się nazywa.

To prawda, że ​​ten dziwny typ jest rzadszy niż błędy, które naprawiłaby twoja propozycja.

Ale z drugiej strony istniejący std::movedziała lepiej w kodzie ogólnym, w którym nie wiesz, czy typ, z którym pracujesz, to a Tczy a T const.


3
+1 za to, że pierwsza odpowiedź, że faktycznie próbuje wyjaśnić, dlaczego byś chciał zadzwonić std::movena constobiekcie.
Chris Drew

+1 za pokazanie wykonywania funkcji const T&&. Wyraża to „protokół API” w rodzaju „wezmę rvalue-ref, ale obiecuję, że go nie zmodyfikuję”. Myślę, że poza używaniem mutable jest to rzadkie. Może innym przypadkiem użycia jest możliwość użycia forward_as_tuplena prawie wszystkim, a później użycia tego.
F Pereira

107

Jest tu pewna sztuczka, którą przeoczasz, a mianowicie, że std::move(cat) tak naprawdę niczego nie rusza . Po prostu mówi kompilatorowi, aby spróbował się przenieść. Jednak ponieważ twoja klasa nie ma konstruktora, który akceptuje a const CAT&&, zamiast tego użyje niejawnego const CAT&konstruktora kopiującego i bezpiecznie skopiuje. Żadnego niebezpieczeństwa, żadnej pułapki. Jeśli konstruktor kopiujący jest wyłączony z jakiegokolwiek powodu, pojawi się błąd kompilatora.

struct CAT
{
   CAT(){}
   CAT(const CAT&) {std::cout << "COPY";}
   CAT(CAT&&) {std::cout << "MOVE";}
};

int main() {
    const CAT cat;
    CAT cat2 = std::move(cat);
}

wydruki COPY, nie MOVE.

http://coliru.stacked-crooked.com/a/0dff72133dbf9d1f

Zauważ, że błąd we wspomnianym kodzie jest problemem z wydajnością , a nie ze stabilnością , więc taki błąd nigdy nie spowoduje awarii. Po prostu użyje wolniejszej kopii. Ponadto taki błąd występuje również w przypadku obiektów niebędących stałymi, które nie mają konstruktorów przenoszenia, więc samo dodanie constprzeciążenia nie złapie ich wszystkich. Moglibyśmy sprawdzić, czy istnieje możliwość przeniesienia konstrukcji lub przeniesienia przypisania z typu parametru, ale mogłoby to kolidować z ogólnym kodem szablonu, który powinien powrócić do konstruktora kopiującego. I do cholery, może ktoś chce móc konstruować const CAT&&, kim ja jestem, żeby powiedzieć, że nie może?


Upticked. Warto zauważyć, że niejawne usunięcie elementu kopiującego po zdefiniowanej przez użytkownika definicji zwykłego konstruktora przenoszenia lub operatora przypisania również zademonstruje to poprzez zepsutą kompilację. Niezła odpowiedź.
WhozCraig

Warto również wspomnieć, że konstruktor kopiujący, który potrzebuje wartości innej niż l, constrównież nie pomoże. [class.copy] §8: "W przeciwnym razie niejawnie zadeklarowany konstruktor kopiujący będzie miał postać X::X(X&)"
Deduplicator,

9
Nie sądzę, żeby miał na myśli „pułapkę” w terminach komputer / montaż. Myślę, że ma na myśli to, że „więzi” go podstępnie podstępnie, ponieważ jeśli nie zdaje sobie sprawy, kończy się z kopią, która nie jest tym, co zamierzał. Chyba ..
Brandon,

czy możesz dostać 1 więcej głosów za, a ja jeszcze 4, za złotą odznakę. ;)
Yakk - Adam Nevraumont

Przybij piątkę w tym przykładzie kodu. Bardzo dobrze ujmuje punkt widzenia.
Brent pisze kod

20

Jednym z powodów, dla których dotychczas pominięto pozostałe odpowiedzi, jest to, że kod ogólny jest odporny na ruch. Na przykład powiedzmy, że chciałem napisać ogólną funkcję, która przenosi wszystkie elementy z jednego rodzaju kontenera, aby utworzyć inny rodzaj kontenera o tych samych wartościach:

template <class C1, class C2>
C1
move_each(C2&& c2)
{
    return C1(std::make_move_iterator(c2.begin()),
              std::make_move_iterator(c2.end()));
}

Fajnie, teraz mogę stosunkowo sprawnie stworzyć vector<string>z a deque<string>i każda osoba stringzostanie przeniesiona w tym procesie.

Ale co, jeśli chcę się przenieść z map?

int
main()
{
    std::map<int, std::string> m{{1, "one"}, {2, "two"}, {3, "three"}};
    auto v = move_each<std::vector<std::pair<int, std::string>>>(m);
    for (auto const& p : v)
        std::cout << "{" << p.first << ", " << p.second << "} ";
    std::cout << '\n';
}

Gdyby std::movenalegać na brak constargumentu, powyższa instancja move_eachnie skompilowałaby się, ponieważ próbuje przenieść a const int( key_typez map). Ale ten kod nie dba o to, czy nie może przenieść pliku key_type. Chce przenieść mapped_type( std::string) ze względu na wydajność.

To w tym przykładzie i niezliczonych innych przykładach podobnych do tego w kodowaniu ogólnym std::movejest to żądanie przeniesienia , a nie żądanie ruchu.


2

Mam te same obawy co OP.

std :: move nie przenosi obiektu, ani nie gwarantuje, że obiekt jest ruchomy. Więc dlaczego nazywa się to ruchem?

Myślę, że brak możliwości ruchu może być jednym z dwóch następujących scenariuszy:

1. Ruchomy typ to const.

Powodem, dla którego mamy słowo kluczowe const w języku jest to, że chcemy, aby kompilator zapobiegał jakiejkolwiek zmianie obiektu zdefiniowanego jako const. Biorąc pod uwagę przykład w książce Scotta Meyersa:

    class Annotation {
    public:
     explicit Annotation(const std::string text)
     : value(std::move(text)) // "move" text into value; this code
     {  } // doesn't do what it seems to!    
     
    private:
     std::string value;
    };

Co to dosłownie oznacza? Przenieś ciąg const do elementu członkowskiego wartości - przynajmniej tak rozumiem, zanim przeczytam wyjaśnienie.

Jeśli język nie zamierza się poruszać lub nie gwarantuje, że ruch ma zastosowanie, gdy wywoływana jest funkcja std :: move (), to jest to dosłownie mylące, gdy używa się słowa move.

Jeśli język zachęca ludzi używających std :: move do lepszej wydajności, musi jak najwcześniej zapobiegać takim pułapkom, szczególnie w przypadku tego typu oczywistej dosłownej sprzeczności.

Zgadzam się, że ludzie powinni mieć świadomość, że przeniesienie stałej jest niemożliwe, ale ten obowiązek nie powinien oznaczać, że kompilator może milczeć, gdy wydarzy się oczywista sprzeczność.

2. Obiekt nie ma konstruktora przenoszenia

Osobiście uważam, że to odrębna historia od troski OP, jak powiedział Chris Drew

@hvd Wydaje mi się, że to trochę bez dyskusji. To, że sugestia OP nie naprawia wszystkich błędów na świecie, niekoniecznie oznacza, że ​​jest to zły pomysł (prawdopodobnie tak jest, ale nie z powodu, który podajesz). - Chris Drew


1

Jestem zaskoczony, że nikt nie wspomniał o aspekcie kompatybilności wstecznej. Uważam, że std::movezostał celowo zaprojektowany do tego w C ++ 11. Wyobraź sobie, że pracujesz ze starszą bazą kodów, która w dużym stopniu opiera się na bibliotekach C ++ 98, więc bez rezerwowego przypisania kopii przenoszenie może zepsuć wszystko.


Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.