Jaka jest różnica między std :: move i std :: forward

160

Widziałem to tutaj: Move Constructor wywołujący klasę podstawową Move Constructor

Czy ktoś mógłby wyjaśnić:

  1. różnica między std::movei std::forward, najlepiej z kilkoma przykładami kodu?
  2. Jak łatwo o tym pomyśleć i kiedy użyć których
aCuria
źródło
1
Zobacz także te dwa powiązane pytania: Czy powinienem używać std :: move lub std :: forward w move ctors / assignment operator? i jak działa std :: forward? (jak również zduplikowane pytanie).
Xeo
„Jak łatwo o tym pomyśleć i kiedy użyć którego” Używasz, movegdy chcesz przenieść wartość i forwardkiedy chcesz użyć idealnego przekazywania. To nie jest fizyka jądrowa;)
Nicol Bolas
move () wykonuje rzutowanie bezwarunkowe, gdzie jako forward () wykonuje rzutowanie na podstawie przekazanego parametru.
RaGa__M

Odpowiedzi:

159

std::movepobiera obiekt i pozwala traktować go jako tymczasowy (wartość r). Chociaż nie jest to wymaganie semantyczne, zwykle funkcja akceptująca odwołanie do wartości r unieważni ją. Kiedy widzisz std::move, oznacza to, że wartość obiektu nie powinna być później używana, ale nadal możesz przypisać nową wartość i nadal jej używać.

std::forwardma pojedynczy przypadek użycia: rzutowanie parametru funkcji z szablonu (wewnątrz funkcji) na kategorię wartości (lwartość lub rwartość), której obiekt wywołujący użył do przekazania. Pozwala to na przekazywanie argumentów r-wartości jako r-wartości, a l-wartości jako l-wartości, co jest schematem nazywanym „przekazywaniem perfekcyjnym”.

Aby zilustrować :

void overloaded( int const &arg ) { std::cout << "by lvalue\n"; }
void overloaded( int && arg ) { std::cout << "by rvalue\n"; }

template< typename t >
/* "t &&" with "t" being template param is special, and  adjusts "t" to be
   (for example) "int &" or non-ref "int" so std::forward knows what to do. */
void forwarding( t && arg ) {
    std::cout << "via std::forward: ";
    overloaded( std::forward< t >( arg ) );
    std::cout << "via std::move: ";
    overloaded( std::move( arg ) ); // conceptually this would invalidate arg
    std::cout << "by simple passing: ";
    overloaded( arg );
}

int main() {
    std::cout << "initial caller passes rvalue:\n";
    forwarding( 5 );
    std::cout << "initial caller passes lvalue:\n";
    int x = 5;
    forwarding( x );
}

Jak wspomina Howard, istnieją również podobieństwa, ponieważ obie te funkcje są po prostu rzutowane na typ referencyjny. Ale poza tymi konkretnymi przypadkami użycia (które obejmują 99,9% użyteczności rzutów referencyjnych rvalue), powinieneś użyć static_castbezpośrednio i napisać dobre wyjaśnienie tego, co robisz.

Potatoswatter
źródło
Nie jestem pewien, że to dokładne, że std::forward„s tylko przypadek użycia jest idealne przekazywanie argumentów funkcji. Spotkałem się z sytuacjami, w których chcę idealnie przekazać inne rzeczy, takie jak elementy składowe obiektów.
Geoff Romer,
@GeoffRomer Członkowie parametru? Masz przykład?
Potatoswatter
Jako osobne pytanie zamieściłem przykład: stackoverflow.com/questions/20616958/…
Geoff Romer
Hmm, więc jeśli dobrze to rozumiem, oznacza to, że mogę napisać pojedynczą funkcję forward (), gdzie forward (5) działa na 5 tak, jakbym przekazał ją przez wartość, gdzie forward (x) działa na x, jakbym ją przekazał odwołanie bez konieczności jawnego pisania przeciążenia.
iheanyi
@iheanyi Tak, taki jest pomysł! Ale musi to być szablon, a nie zwykła funkcja.
Potatoswatter
63

Obie std::forwardi std::movesą niczym innym jak odlewami.

X x;
std::move(x);

Powyższe rzutuje wyrażenie l- xwartości typu X na wyrażenie r-wartości typu X (dokładnie xwartość). movemoże również akceptować wartość r:

std::move(make_X());

w tym przypadku jest to funkcja tożsamości: przyjmuje wartość r typu X i zwraca wartość r typu X.

Dzięki std::forwardniemu możesz w pewnym stopniu wybrać miejsce docelowe:

X x;
std::forward<Y>(x);

Rzuca wyrażenie l- xwartości typu X na wyrażenie typu Y. Istnieją ograniczenia dotyczące tego, czym może być Y.

Y może być dostępną bazą X lub odwołaniem do podstawy X. Y może oznaczać X lub odniesienie do X. Nie można odrzucać kwalifikatorów cv forward, ale można dodać kwalifikatory cv. Y nie może być typem, który można jedynie zamienić z X, z wyjątkiem dostępnej konwersji Base.

Jeśli Y jest odwołaniem do lwartości, wynik będzie wyrażeniem lwartości. Jeśli Y nie jest odniesieniem do lwartości, wynik będzie wyrażeniem rvalue (a dokładniej xvalue).

forwardmoże przyjąć argument rvalue tylko wtedy, gdy Y nie jest odniesieniem do lwartości. Oznacza to, że nie można rzutować wartości r na lwartość. Dzieje się tak ze względów bezpieczeństwa, ponieważ często prowadzi to do zawieszania się odniesień. Ale rzutowanie rvalue na rvalue jest w porządku i dozwolone.

Jeśli spróbujesz określić Y jako coś, co jest niedozwolone, błąd zostanie przechwycony w czasie kompilacji, a nie w czasie wykonywania.

Howard Hinnant
źródło
Jeśli perfekcyjnie przekażę obiekt do funkcji używającej std::forward, to czy po wykonaniu tej funkcji mogę użyć tego obiektu? Zdaję sobie sprawę, że w przypadku std::movejest to niezdefiniowane zachowanie.
iammilind,
1
Odnośnie move: stackoverflow.com/a/7028318/576911 uzyskać forward, jeśli przechodzą w lwartością Twój API powinien reagować tak, jakby odbiera lwartością. Zwykle oznacza to, że wartość nie zostanie zmieniona. Ale jeśli jest to wartość inna niż stała l, Twój interfejs API mógł ją zmodyfikować. Jeśli przekażesz rvalue, zwykle oznacza to, że Twój interfejs API mógł się z niej przenieść, a zatem zastosowanie miałoby stackoverflow.com/a/7028318/576911 .
Howard Hinnant
21

std::forwardsłuży do przekazywania parametru dokładnie w taki sposób, w jaki został przekazany do funkcji. Tak jak pokazano tutaj:

Kiedy używać std :: forward do przekazywania argumentów?

Użycie std::moveoferuje obiekt jako rvalue, aby ewentualnie dopasować konstruktor przenoszenia lub funkcję akceptującą rvalues. Robi tak, std::move(x)nawet jeśli xsama w sobie nie jest wartością r.

Bo Persson
źródło