Dlaczego „std :: move” nosi nazwę „std :: move”?

128

Funkcja C ++ 11 std::move(x)tak naprawdę niczego nie przenosi. To tylko rzut na wartość r. Dlaczego to zrobiono? Czy to nie jest mylące?

Howard Hinnant
źródło
Co gorsza, std::move
trójargumentowy
I nie zapomnij o C ++ std::char_traits::move
98/03/11
30
Moim innym ulubionym jest to, std::remove()że nie usuwa elementów: nadal musisz wywołać, erase()aby faktycznie usunąć te elementy z kontenera. Więc movenie rusza się, removenie usuwa. Wybrałbym nazwę mark_movable()dla move.
Ali
4
@Ali również byłby mark_movable()mylący. Sugeruje, że istnieje trwały efekt uboczny, podczas gdy w rzeczywistości go nie ma.
finnw

Odpowiedzi:

177

Prawdą jest, że std::move(x)jest to tylko rzutowanie na rwartość - a dokładniej na xwartość , w przeciwieństwie do prwartości . Prawdą jest również, że imię obsady moveczasami dezorientuje ludzi. Jednak celem tego nazewnictwa nie jest zmylenie, ale raczej uczynienie kodu bardziej czytelnym.

Historia movesięga do pierwotnej propozycji przeniesienia z 2002 roku . Ten artykuł najpierw wprowadza odniesienie do wartości r, a następnie pokazuje, jak napisać bardziej wydajne std::swap:

template <class T>
void
swap(T& a, T& b)
{
    T tmp(static_cast<T&&>(a));
    a = static_cast<T&&>(b);
    b = static_cast<T&&>(tmp);
}

Należy przypomnieć, że w tym momencie historii jedyną rzeczą, która &&mogłaby oznaczać, była logika i . Nikt nie był zaznajomiony z odniesieniami do rwartości, ani z implikacjami rzutowania lwartości na rwartość (nie robiąc kopii tak static_cast<T>(t), jak by to zrobił). Czytelnicy tego kodu naturalnie pomyśleliby:

Wiem, jak swapma działać (kopiowanie na tymczasowe, a potem zamiana wartości), ale jaki jest cel tych brzydkich odlewów ?!

Zauważ również, że swapjest to tak naprawdę tylko zastępstwo dla wszelkiego rodzaju algorytmów modyfikujących permutacje. Ta dyskusja jest dużo , dużo większa niż swap.

Następnie propozycja wprowadza cukier składniowy, który zastępuje static_cast<T&&>coś bardziej czytelnym, co nie wyjaśnia dokładnie, co , ale raczej dlaczego :

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

To moveznaczy jest po prostu cukierkiem składniowym static_cast<T&&>, a teraz kod sugeruje, dlaczego istnieją te rzutowania: aby włączyć semantykę przenoszenia!

Trzeba zrozumieć, że w kontekście historii niewiele osób w tym momencie naprawdę zrozumiało intymny związek między wartościami r i semantyką ruchu (choć artykuł próbuje to również wyjaśnić):

Po podaniu argumentów rvalue automatycznie włącza się semantyka ruchu. Jest to całkowicie bezpieczne, ponieważ przenoszenie zasobów z wartości r nie może zostać zauważone przez resztę programu ( nikt inny nie ma odniesienia do wartości r w celu wykrycia różnicy ).

Jeśli w tamtym czasie swapzostał przedstawiony w ten sposób:

template <class T>
void
swap(T& a, T& b)
{
    T tmp(cast_to_rvalue(a));
    a = cast_to_rvalue(b);
    b = cast_to_rvalue(tmp);
}

Wtedy ludzie spojrzeliby na to i powiedzieli:

Ale dlaczego rzucasz na rvalue?


Główny punkt:

Tak jak było, używając move, nikt nigdy nie zapytał:

Ale dlaczego się przeprowadzasz?


W miarę upływu lat i udoskonalania propozycji, pojęcia lwartości i rwartości zostały dopracowane do kategorii wartości, które mamy dzisiaj:

Taksonomia

(obraz bezwstydnie skradziony z dirkgently )

I tak dzisiaj, gdybyśmy chcieli swapprecyzyjnie powiedzieć, co robi, zamiast dlaczego , powinien wyglądać bardziej tak:

template <class T>
void
swap(T& a, T& b)
{
    T tmp(set_value_category_to_xvalue(a));
    a = set_value_category_to_xvalue(b);
    b = set_value_category_to_xvalue(tmp);
}

I pytanie, które każdy powinien sobie zadać, brzmi: czy powyższy kod jest mniej lub bardziej czytelny niż:

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

Albo nawet oryginał:

template <class T>
void
swap(T& a, T& b)
{
    T tmp(static_cast<T&&>(a));
    a = static_cast<T&&>(b);
    b = static_cast<T&&>(tmp);
}

W każdym razie czeladnik programista C ++ powinien wiedzieć, że pod maską movenie dzieje się nic więcej niż obsada. A początkujący programista C ++, przynajmniej z move, zostanie poinformowany, że jego zamiarem jest przejście z prawej strony, a nie kopiowanie z prawej strony, nawet jeśli nie rozumieją dokładnie, jak to się robi .

Dodatkowo, jeśli programista życzy sobie tej funkcjonalności pod inną nazwą, std::movenie ma monopolu na tę funkcjonalność i nie ma nieprzenośnej magii językowej związanej z jej implementacją. Na przykład, jeśli ktoś chciałby kodować set_value_category_to_xvaluei zamiast tego używać tego, jest to trywialne:

template <class T>
inline
constexpr
typename std::remove_reference<T>::type&&
set_value_category_to_xvalue(T&& t) noexcept
{
    return static_cast<typename std::remove_reference<T>::type&&>(t);
}

W C ++ 14 robi się jeszcze bardziej zwięźle:

template <class T>
inline
constexpr
auto&&
set_value_category_to_xvalue(T&& t) noexcept
{
    return static_cast<std::remove_reference_t<T>&&>(t);
}

Więc jeśli masz taką skłonność, udekoruj swój sposób, w static_cast<T&&>jaki myślisz najlepiej, a być może w końcu opracujesz nową najlepszą praktykę (C ++ stale się rozwija).

Więc co robi movew zakresie wygenerowanego kodu obiektowego?

Rozważ to test:

void
test(int& i, int& j)
{
    i = j;
}

Skompilowane za pomocą clang++ -std=c++14 test.cpp -O3 -S, generuje ten kod wynikowy:

__Z4testRiS_:                           ## @_Z4testRiS_
    .cfi_startproc
## BB#0:
    pushq   %rbp
Ltmp0:
    .cfi_def_cfa_offset 16
Ltmp1:
    .cfi_offset %rbp, -16
    movq    %rsp, %rbp
Ltmp2:
    .cfi_def_cfa_register %rbp
    movl    (%rsi), %eax
    movl    %eax, (%rdi)
    popq    %rbp
    retq
    .cfi_endproc

Teraz, jeśli test zostanie zmieniony na:

void
test(int& i, int& j)
{
    i = std::move(j);
}

Nie ma absolutnie żadnej zmiany w kodzie wynikowym. Wynik ten można uogólnić tak, że: dla obiektów trywialnie poruszających sięstd::move nie ma żadnego wpływu.

Spójrzmy teraz na ten przykład:

struct X
{
    X& operator=(const X&);
};

void
test(X& i, X& j)
{
    i = j;
}

To generuje:

__Z4testR1XS0_:                         ## @_Z4testR1XS0_
    .cfi_startproc
## BB#0:
    pushq   %rbp
Ltmp0:
    .cfi_def_cfa_offset 16
Ltmp1:
    .cfi_offset %rbp, -16
    movq    %rsp, %rbp
Ltmp2:
    .cfi_def_cfa_register %rbp
    popq    %rbp
    jmp __ZN1XaSERKS_           ## TAILCALL
    .cfi_endproc

W przypadku uruchomienia __ZN1XaSERKS_przez c++filtprodukuje: X::operator=(X const&). Nic dziwnego. Teraz, jeśli test zostanie zmieniony na:

void
test(X& i, X& j)
{
    i = std::move(j);
}

Wtedy nadal nie ma żadnej zmiany w wygenerowanym kodzie obiektowym. std::movenie zrobił nic poza rzutowaniem jna wartość r, a następnie ta wartość r Xwiąże się z operatorem przypisania kopiowania z X.

Teraz dodajmy operator przypisania przenoszenia do X:

struct X
{
    X& operator=(const X&);
    X& operator=(X&&);
};

Teraz kod obiektowy się zmienia:

__Z4testR1XS0_:                         ## @_Z4testR1XS0_
    .cfi_startproc
## BB#0:
    pushq   %rbp
Ltmp0:
    .cfi_def_cfa_offset 16
Ltmp1:
    .cfi_offset %rbp, -16
    movq    %rsp, %rbp
Ltmp2:
    .cfi_def_cfa_register %rbp
    popq    %rbp
    jmp __ZN1XaSEOS_            ## TAILCALL
    .cfi_endproc

Przejście __ZN1XaSEOS_przez c++filtobjawienia, które X::operator=(X&&)są wywoływane zamiast X::operator=(X const&).

I to wszystko std::move! Znika całkowicie w czasie wykonywania. Jego jedyny wpływ ma miejsce w czasie kompilacji, gdzie może zmienić wywołanie przeciążenia.

Howard Hinnant
źródło
7
Oto źródło kropka na tym wykresie: odtworzyłem go digraph D { glvalue -> { lvalue; xvalue } rvalue -> { xvalue; prvalue } expression -> { glvalue; rvalue } }dla dobra publicznego :) go pobrać tutaj jako SVG
sehe
7
Czy to nadal jest otwarte dla rowerów? Proponuję allow_move;)
dyp
2
@dyp Moim ulubionym jest nadal movable.
Daniel Frey,
6
Scott Meyers zasugerował zmianę nazwy std::movena rvalue_cast: youtube.com/…
nairware
6
Ponieważ rwartość odnosi się teraz zarówno do prwartości, jak i do wartości x, rvalue_castjest niejednoznaczne w swoim znaczeniu: jaki rodzaj rwartości zwraca? xvalue_castbyłoby tutaj spójną nazwą. Niestety, większość ludzi w tym czasie również nie rozumiałaby, co robi. Mam nadzieję, że za kilka lat moje oświadczenie stanie się fałszywe.
Howard Hinnant
20

Pozwólcie, że zostawię tutaj cytat z FAQ C ++ 11 napisanego przez B. Stroustrupa, który jest bezpośrednią odpowiedzią na pytanie OP:

move (x) oznacza „możesz traktować x jako wartość r”. Może byłoby lepiej, gdyby move () nazywał się rval (), ale do tej pory move () był używany od lat.

Nawiasem mówiąc, bardzo podobały mi się FAQ - warto je przeczytać.

podkova
źródło
2
Aby plagiatować komentarz @ HowardHinnant z innej odpowiedzi: odpowiedź Stroustrupa jest niedokładna, ponieważ istnieją teraz dwa rodzaje r-wartości - prvalues ​​i xvalues, a std :: move jest w rzeczywistości rzutem xvalue.
einpoklum