Co to jest std :: move () i kiedy należy go używać?

656
  1. Co to jest?
  2. Co to robi?
  3. Kiedy należy go używać?

Doceniamy dobre linki.

Basilevs
źródło
42
Bjarne Stroustrup wyjaśnia ruch w Krótkim wstępie do odniesień do wartości
DumbCoder
1
Przenieś semantykę
Basilevs,
12
To pytanie dotyczy std::move(T && t); istnieje również std::move(InputIt first, InputIt last, OutputIt d_first)algorytm związany z std::copy. Zwracam na to uwagę, aby inni nie byli tak zdezorientowani jak ja, kiedy po raz pierwszy skonfrontowałem się z std::moveprzyjęciem trzech argumentów. en.cppreference.com/w/cpp/algorithm/move
josaphatv

Odpowiedzi:

286

Strona Wikipedii na temat C ++ 11 Odnośniki do wartości R i konstruktory ruchów

  1. W C ++ 11, oprócz konstruktorów kopiowania, obiekty mogą mieć konstruktory ruchów.
    (Oprócz operatorów kopiowania przypisania mają także operatory przypisania przeniesienia).
  2. Konstruktor ruchu jest używany zamiast konstruktora kopiowania, jeśli obiekt ma typ „rvalue-reference” ( Type &&).
  3. std::move() jest rzutowaniem, które generuje odwołanie do wartości do obiektu, aby umożliwić poruszanie się z niego.

Jest to nowy sposób C ++ pozwalający uniknąć kopii. Na przykład, używając konstruktora przenoszenia, a std::vectormoże po prostu skopiować swój wewnętrzny wskaźnik do danych do nowego obiektu, pozostawiając przeniesiony obiekt w stanie przeniesionym ze stanu, a zatem nie kopiując wszystkich danych. To byłoby C ++ - poprawne.

Spróbuj googlingu, aby uzyskać semantykę ruchu, wartość rue, doskonałe przekazywanie.

Scharron
źródło
39
Semantyka Move wymaga, aby przeniesiony obiekt pozostał ważny , co nie jest stanem niepoprawnym. (Uzasadnienie: To wciąż musi
niszczyć
13
@GMan: cóż, musi być w stanie, który jest bezpieczny do zniszczenia, ale, AFAIK, nie musi być przydatny do niczego innego.
Zan Lynx,
8
@ZanLynx: Racja. Zauważ, że biblioteka standardowa wymaga dodatkowo przypisania przeniesionych obiektów, ale dotyczy to tylko obiektów używanych w standardzie, nie jest to ogólny wymóg.
GManNickG
24
-1 „std :: move () jest sposobem C ++ 11 na użycie semantyki move” Proszę to naprawić. std::move()nie jest sposobem używania semantyki ruchów, semantyka ruchów jest wykonywana dla programisty w sposób transparentny. movejest to tylko rzutowanie, które przekazuje wartość z jednego punktu do drugiego, gdzie oryginalna wartość nie będzie już używana.
Manu343726,
15
Poszedłbym dalej std::movesamo w sobie „nic” - ma zerowe skutki uboczne. To tylko sygnalizuje kompilatorowi, że programista nie dba już o to, co stanie się z tym obiektem. tzn. zezwala innym częściom oprogramowania na przemieszczanie się z obiektu, ale nie wymaga przenoszenia. W rzeczywistości odbiorca odwołania do wartości nie musi składać żadnych obietnic dotyczących tego, co zrobi lub nie zrobi z danymi.
Aaron McDaid,
240

1. „Co to jest?”

Chociaż std::move() technicznie jest to funkcja - powiedziałbym, że tak naprawdę nie jest funkcją . Jest to rodzaj konwertera między sposobami, w których kompilator uwzględnia wartość wyrażenia.

2. „Co to robi?”

Pierwszą rzeczą, na którą należy zwrócić uwagę, jest to, że std::move() tak naprawdę nic nie rusza . Konwertuje wyrażenie od bycia lwartość (takich jak zmiennej o nazwie) do bycia xvalue . Wartość x mówi kompilatorowi:

Możesz mnie ograbić, przenieść wszystko, co trzymam i użyć go gdzie indziej (bo i tak wkrótce mnie zniszczą).

innymi słowy, kiedy używasz std::move(x), pozwalasz kompilatorowi na kanibalizację x. Zatem jeśli x, powiedzmy, ma własny bufor w pamięci - po std::move()kompilacji może zamiast niego posiadać inny obiekt.

Możesz także przejść z prvalue (np. Tymczasowego mijania), ale jest to rzadko przydatne.

3. „Kiedy należy go użyć?”

Innym sposobem na zadanie tego pytania jest: „Po co miałbym kanibalizować zasoby istniejącego obiektu?” cóż, jeśli piszesz kod aplikacji, prawdopodobnie nie będziesz się zbytnio bawić w tymczasowe obiekty tworzone przez kompilator. Zasadniczo robiłbyś to w miejscach takich jak konstruktory, metody operatora, funkcje podobne do algorytmów biblioteki standardowej itp., W których obiekty są często tworzone i niszczone automatycznie. Oczywiście to tylko ogólna zasada.

Typowym zastosowaniem jest „przenoszenie” zasobów z jednego obiektu do drugiego zamiast kopiowania. @Guillaume prowadzi do tej strony, która ma prosty krótki przykład: zamiana dwóch obiektów przy mniejszym kopiowaniu.

template <class T>
swap(T& a, T& b) {
    T tmp(a);   // we now have two copies of a
    a = b;      // we now have two copies of b (+ discarded a copy of a)
    b = tmp;    // we now have two copies of tmp (+ discarded a copy of b)
}

użycie move pozwala zamienić zasoby zamiast je kopiować:

template <class T>
swap(T& a, T& b) {
    T tmp(std::move(a));
    a = std::move(b);   
    b = std::move(tmp);
}

Pomyśl o tym, co się stanie, gdy Tpowiedzmy vector<int>rozmiar n. W pierwszej wersji czytasz i zapisujesz 3 * n elementów, w drugiej wersji w zasadzie odczytujesz i zapisujesz tylko 3 wskaźniki do buforów wektorów oraz rozmiary 3 buforów. Oczywiście klasa Tmusi wiedzieć, jak się poruszać; twoja klasa powinna mieć operator przypisania ruchu i konstruktor ruchu, aby klasa Tdziałała.

einpoklum
źródło
3
Od dawna słyszałem o semantyce ruchów, nigdy nie zaglądałem w nie. Z podanego opisu wydaje się, że jest to płytka kopia zamiast głębokiej kopii.
Zebrafish
7
@TitoneMaurice: Tyle że nie jest to kopia - ponieważ pierwotna wartość nie jest już użyteczna.
einpoklum
3
@Zebrafish, nie możesz się bardziej mylić. Płytka kopia pozostawia oryginał w dokładnie tym samym stanie, ruch zwykle powoduje, że oryginał jest pusty lub znajduje się w innym ważnym stanie.
rubenvb 10.01.2018
16
@rubenvb Zebra nie jest całkowicie błędna. Chociaż prawdą jest, że oryginalny kanabiliowany obiekt jest zwykle celowo sabotowany, aby uniknąć mylących błędów (np. Ustawić jego wskaźniki na nullptr, aby zasygnalizować, że nie jest już posiadaczem pointee), faktem jest, że cały ruch jest wykonywany po prostu przez skopiowanie wskaźnika ze źródła do miejsca docelowego (i celowe unikanie robienia czegokolwiek z osobą wskazującą) w rzeczywistości przypomina płytką kopię. W rzeczywistości posunąłbym się do stwierdzenia, że ​​ruch jest płytką kopią, po której opcjonalnie następuje częściowe samozniszczenie źródła. (cd.)
Wyścigi lekkości na orbicie
3
(cd.) Jeśli pozwolimy na tę definicję (a raczej ją lubię), to obserwacja @ Zebrafisha nie jest błędna, tylko nieco niekompletna.
Wyścigi lekkości na orbicie
145

Możesz użyć move, gdy potrzebujesz „przenieść” zawartość obiektu w inne miejsce, bez robienia kopii (tzn. Treść nie jest powielana, dlatego może być używana na niektórych obiektach, których nie można skopiować, np. Unikatowa_ptr). Możliwe jest również, że obiekt pobierze zawartość obiektu tymczasowego bez robienia kopii (i oszczędza dużo czasu), przy pomocy std :: move.

Ten link naprawdę mi pomógł:

http://thbecker.net/articles/rvalue_references/section_01.html

Przepraszam, jeśli moja odpowiedź przychodzi za późno, ale szukałem również dobrego linku do std :: move, i znalazłem linki powyżej nieco „surowe”.

To kładzie nacisk na odniesienie do wartości r, w którym kontekście powinieneś ich użyć, i myślę, że jest bardziej szczegółowe, dlatego chciałem udostępnić ten link tutaj.

Guillaume
źródło
26
Niezły link. Zawsze znajdowałem artykuł na Wikipedii i inne linki, na które natknąłem się dość myląco, ponieważ po prostu rzucają fakty na ciebie, pozostawiając ci ustalenie, jakie jest rzeczywiste znaczenie / uzasadnienie. Podczas gdy „semantyka przenoszenia” w konstruktorze jest dość oczywista, wszystkie te szczegóły dotyczące przekazywania && - wartości wokół nie są ... więc opis w stylu samouczka był bardzo miły.
Christian Stieber,
66

P: Co to jest std::move ?

ZA: std::move() jest funkcją z biblioteki standardowej C ++ do rzutowania na odwołanie do wartości.

Uproszczony std::move(t)jest równoważny z:

static_cast<T&&>(t);

Wartość jest wartością tymczasową, która nie utrzymuje się poza wyrażeniem, które ją definiuje, takim jak wynik funkcji pośredniej, który nigdy nie jest przechowywany w zmiennej.

int a = 3; // 3 is a rvalue, does not exist after expression is evaluated
int b = a; // a is a lvalue, keeps existing after expression is evaluated

Implementacja std :: move () jest podana w N2027: „Krótkie wprowadzenie do odwołań do wartości” w następujący sposób:

template <class T>
typename remove_reference<T>::type&&
std::move(T&& a)
{
    return a;
}

Jak widać, std::movezwraca T&&bez względu na to, czy zostanie wywołana z wartością ( T), typem referencji ( T&) czy wartością referencyjną wartości ( T&&).

P: Co to robi?

Odp .: Jako obsada nic nie robi podczas działania. Ważne jest tylko w czasie kompilacji, aby poinformować kompilator, że chcesz nadal traktować referencję jako wartość.

foo(3 * 5); // obviously, you are calling foo with a temporary (rvalue)

int a = 3 * 5;
foo(a);     // how to tell the compiler to treat `a` as an rvalue?
foo(std::move(a)); // will call `foo(int&& a)` rather than `foo(int a)` or `foo(int& a)`

Czego nie robi:

  • Zrób kopię argumentu
  • Wywołaj konstruktor kopiowania
  • Zmień obiekt argumentu

P: Kiedy należy go używać?

Odp .: Powinieneś użyć, std::movejeśli chcesz wywoływać funkcje obsługujące semantykę przenoszenia z argumentem, który nie jest wartością (wyrażenie tymczasowe).

To rodzi dla mnie następujące pytania uzupełniające:

  • Co to jest semantyka ruchu? Semantyka przenoszenia w przeciwieństwie do semantyki kopiowania jest techniką programowania, w której elementy obiektu są inicjowane przez „przejęcie” zamiast kopiowania elementów innego obiektu. Takie „przejmowanie” ma sens tylko w przypadku wskaźników i uchwytów zasobów, które można tanio przenosić, kopiując uchwyt wskaźnika lub liczby całkowitej zamiast bazowych danych.

  • Jakie klasy i obiekty obsługują semantykę ruchów? Od programisty należy wdrożenie semantyki przenoszenia we własnych klasach, jeśli skorzystałoby z przeniesienia członków zamiast ich kopiowania. Po zaimplementowaniu semantyki przenoszenia bezpośrednio skorzystasz z pracy wielu programistów bibliotek, którzy dodali obsługę efektywnego zarządzania klasami z semantyką przenoszenia.

  • Dlaczego kompilator nie może tego sam zrozumieć? Kompilator nie może po prostu wywołać kolejnego przeciążenia funkcji, chyba że tak powiesz. Musisz pomóc kompilatorowi wybrać, czy ma być wywoływana wersja zwykła czy przeniesiona funkcji.

  • W jakich sytuacjach chciałbym powiedzieć kompilatorowi, że powinien traktować zmienną jak wartość? Najprawdopodobniej stanie się to w funkcjach szablonów lub bibliotek, gdzie wiesz, że można uzyskać wynik pośredni.

Christopher Oezbek
źródło
2
Duża +1 dla przykładów kodu z semantyką w komentarzach. Pozostałe najważniejsze odpowiedzi definiują std :: move za pomocą samego „move” - tak naprawdę nic nie wyjaśnia! --- Uważam, że warto wspomnieć, że nie zrobienie kopii argumentu oznacza, że ​​pierwotnej wartości nie można wiarygodnie wykorzystać.
ty
34

sam std :: move nie robi wiele. Myślałem, że wywołuje on konstruktor ruchu dla obiektu, ale tak naprawdę wykonuje rzut typu (rzutowanie zmiennej lvalue na wartość, aby wymieniona zmienna mogła zostać przekazana jako argument do konstruktora ruchu lub operatora przypisania).

Zatem std :: move jest po prostu używany jako prekursor do użycia semantyki move. Semantyka przesuwania jest zasadniczo skutecznym sposobem radzenia sobie z obiektami tymczasowymi.

Rozważ obiekt A = B + C + D + E + F;

To ładnie wyglądający kod, ale E + F tworzy obiekt tymczasowy. Następnie D + temp tworzy kolejny obiekt tymczasowy i tak dalej. W każdym normalnym operatorze „+” klasy występują głębokie kopie.

Na przykład

Object Object::operator+ (const Object& rhs) {
    Object temp (*this);
    // logic for adding
    return temp;
}

Utworzenie obiektu tymczasowego w tej funkcji jest bezużyteczne - te obiekty tymczasowe zostaną usunięte na końcu linii, gdy wyjdą poza zakres.

Możemy raczej użyć semantyki ruchu, aby „splądrować” obiekty tymczasowe i zrobić coś podobnego

 Object& Object::operator+ (Object&& rhs) {
     // logic to modify rhs directly
     return rhs;
 }

Pozwala to uniknąć niepotrzebnych głębokich kopii. W odniesieniu do przykładu jedyną częścią, w której zachodzi głębokie kopiowanie, jest teraz E + F. Reszta używa semantyki przenoszenia. Konstruktor ruchu lub operator przypisania również musi zostać zaimplementowany, aby przypisać wynik do A.

użytkownik929404
źródło
3
mówiłeś o semantyce ruchów. powinieneś dodać do swojej odpowiedzi, jak można używać std :: move, ponieważ pytanie o to pyta.
Koushik Shetty
2
@Koushik std :: move nie robi wiele - ale służy do implementacji semantyki move. Jeśli nie wiesz o std :: move, prawdopodobnie nie znasz też semantyki move
user929404
1
„nie robi wiele” (tak, tylko static_cast do odwołania do wartości). to, co faktycznie robi i robi to, o co poprosił PO. nie musisz wiedzieć, jak działa std :: move, ale wiesz, co robi semantyka move. ponadto, „ale jest stosowany do implementacji semantyki ruchu”, jest odwrotnie. znasz semantykę ruchu, a zrozumiesz std :: move inaczej nie. ruch tylko pomaga w ruchu i sam używa semantyki ruchu. std :: move robi tylko konwersję argumentu na odwołanie do wartości, czego wymaga semantyka przenoszenia.
Koushik Shetty
10
„ale E + F tworzy obiekt tymczasowy” - Operator +przesuwa się od lewej do prawej, a nie od prawej do lewej. Stąd B+Cbyłby pierwszy!
Ajay,
8

"Co to jest?" i „Co to robi?”zostało wyjaśnione powyżej.

Podam przykład „kiedy należy go użyć”.

Na przykład mamy klasę z dużą ilością zasobów, takich jak duża tablica.

class ResHeavy{ //  ResHeavy means heavy resource
    public:
        ResHeavy(int len=10):_upInt(new int[len]),_len(len){
            cout<<"default ctor"<<endl;
        }

        ResHeavy(const ResHeavy& rhs):_upInt(new int[rhs._len]),_len(rhs._len){
            cout<<"copy ctor"<<endl;
        }

        ResHeavy& operator=(const ResHeavy& rhs){
            _upInt.reset(new int[rhs._len]);
            _len = rhs._len;
            cout<<"operator= ctor"<<endl;
        }

        ResHeavy(ResHeavy&& rhs){
            _upInt = std::move(rhs._upInt);
            _len = rhs._len;
            rhs._len = 0;
            cout<<"move ctor"<<endl;
        }

    // check array valid
    bool is_up_valid(){
        return _upInt != nullptr;
    }

    private:
        std::unique_ptr<int[]> _upInt; // heavy array resource
        int _len; // length of int array
};

Kod testowy:

void test_std_move2(){
    ResHeavy rh; // only one int[]
    // operator rh

    // after some operator of rh, it becomes no-use
    // transform it to other object
    ResHeavy rh2 = std::move(rh); // rh becomes invalid

    // show rh, rh2 it valid
    if(rh.is_up_valid())
        cout<<"rh valid"<<endl;
    else
        cout<<"rh invalid"<<endl;

    if(rh2.is_up_valid())
        cout<<"rh2 valid"<<endl;
    else
        cout<<"rh2 invalid"<<endl;

    // new ResHeavy object, created by copy ctor
    ResHeavy rh3(rh2);  // two copy of int[]

    if(rh3.is_up_valid())
        cout<<"rh3 valid"<<endl;
    else
        cout<<"rh3 invalid"<<endl;
}

wyjście jak poniżej:

default ctor
move ctor
rh invalid
rh2 valid
copy ctor
rh3 valid

Możemy to zobaczyć za std::movepomocąmove constructor marki przekształcić zasób łatwo.

Gdzie jeszcze jest std::move przydaje?

std::movemoże być również przydatny podczas sortowania tablicy elementów. Wiele algorytmów sortowania (takich jak sortowanie selekcyjne i bąbelkowe) działa poprzez zamianę par elementów. Wcześniej musieliśmy używać semantyki kopiowania, aby wykonać wymianę. Teraz możemy użyć semantyki ruchu, która jest bardziej wydajna.

Może być również przydatny, jeśli chcemy przenieść zawartość zarządzaną przez jeden inteligentny wskaźnik do drugiego.

Cytowane:

Jayhello
źródło
0

Oto pełny przykład użycia std :: move dla (prostego) niestandardowego wektora

Oczekiwany wynik:

 c: [10][11]
 copy ctor called
 copy of c: [10][11]
 move ctor called
 moved c: [10][11]

Kompiluj jako:

  g++ -std=c++2a -O2 -Wall -pedantic foo.cpp

Kod:

#include <iostream>
#include <algorithm>

template<class T> class MyVector {
private:
    T *data;
    size_t maxlen;
    size_t currlen;
public:
    MyVector<T> () : data (nullptr), maxlen(0), currlen(0) { }
    MyVector<T> (int maxlen) : data (new T [maxlen]), maxlen(maxlen), currlen(0) { }

    MyVector<T> (const MyVector& o) {
        std::cout << "copy ctor called" << std::endl;
        data = new T [o.maxlen];
        maxlen = o.maxlen;
        currlen = o.currlen;
        std::copy(o.data, o.data + o.maxlen, data);
    }

    MyVector<T> (const MyVector<T>&& o) {
        std::cout << "move ctor called" << std::endl;
        data = o.data;
        maxlen = o.maxlen;
        currlen = o.currlen;
    }

    void push_back (const T& i) {
        if (currlen >= maxlen) {
            maxlen *= 2;
            auto newdata = new T [maxlen];
            std::copy(data, data + currlen, newdata);
            if (data) {
                delete[] data;
            }
            data = newdata;
        }
        data[currlen++] = i;
    }

    friend std::ostream& operator<<(std::ostream &os, const MyVector<T>& o) {
        auto s = o.data;
        auto e = o.data + o.currlen;;
        while (s < e) {
            os << "[" << *s << "]";
            s++;
        }
        return os;
    }
};

int main() {
    auto c = new MyVector<int>(1);
    c->push_back(10);
    c->push_back(11);
    std::cout << "c: " << *c << std::endl;
    auto d = *c;
    std::cout << "copy of c: " << d << std::endl;
    auto e = std::move(*c);
    delete c;
    std::cout << "moved c: " << e << std::endl;
}
Neil McGill
źródło