Próbuję zrozumieć odwołania do wartości i przenieść semantykę C ++ 11.
Jaka jest różnica między tymi przykładami i który z nich nie wykona kopii wektorowej?
Pierwszy przykład
std::vector<int> return_vector(void)
{
std::vector<int> tmp {1,2,3,4,5};
return tmp;
}
std::vector<int> &&rval_ref = return_vector();
Drugi przykład
std::vector<int>&& return_vector(void)
{
std::vector<int> tmp {1,2,3,4,5};
return std::move(tmp);
}
std::vector<int> &&rval_ref = return_vector();
Trzeci przykład
std::vector<int> return_vector(void)
{
std::vector<int> tmp {1,2,3,4,5};
return std::move(tmp);
}
std::vector<int> &&rval_ref = return_vector();
c++
c++11
move-semantics
rvalue-reference
c++-faq
Tarantula
źródło
źródło
std::move()
utworzono trwałą „kopię”.std::move(expression)
niczego nie tworzy, po prostu rzutuje wyrażenie na wartość x. Żadne obiekty nie są kopiowane ani przenoszone w trakcie ocenystd::move(expression)
.Odpowiedzi:
Pierwszy przykład
Pierwszy przykład zwraca wartość tymczasową, która jest przechwytywana przez
rval_ref
. Tymczasowe życie wydłuży się pozarval_ref
definicję i możesz go używać tak, jakbyś złapał go pod względem wartości. Jest to bardzo podobne do następującego:poza tym, że w moim przepisywaniu nie można oczywiście używać
rval_ref
w sposób niezmienny.Drugi przykład
W drugim przykładzie utworzono błąd czasu wykonywania.
rval_ref
teraz zawiera odniesienie do zniszczonejtmp
funkcji. Przy odrobinie szczęścia ten kod natychmiast się zawiesi.Trzeci przykład
Twój trzeci przykład jest mniej więcej taki sam jak twój pierwszy.
std::move
Natmp
to niepotrzebne i może być pessimization wydajność jak to będzie hamować optymalizacji zwracanej wartości.Najlepszym sposobem na kodowanie tego, co robisz, jest:
Najlepsze praktyki
Tj. Tak jak w C ++ 03.
tmp
jest domyślnie traktowany jako wartość w instrukcji return. Zostanie on zwrócony poprzez optymalizację wartości zwracanej (bez kopiowania, bez przenoszenia), lub jeśli kompilator zdecyduje, że nie może wykonać RVO, użyje konstruktora ruchu wektora, aby dokonać zwrotu . Tylko jeśli RVO nie zostanie wykonane i jeśli zwracany typ nie miał konstruktora przenoszenia, do zwrotu zostanie użyty konstruktor kopiowania.źródło
return my_local;
. Wiele instrukcji zwrotu jest w porządku i nie będzie hamować RVO.move
nie tworzy tymczasowego. Rzuca wartość na wartość x, nie robi kopii, nie tworzy niczego, niczego nie niszczy. Ten przykład jest dokładnie taką samą sytuacją, jak w przypadku powrotu przez odwołanie do wartości i usunięciemove
wiersza zwrotnego: Tak czy inaczej, masz wiszące odwołanie do zmiennej lokalnej wewnątrz funkcji i która została zniszczona.Żadne z nich nie zostanie skopiowane, ale drugie odniesie się do zniszczonego wektora. Nazwane referencje wartości prawie nigdy nie istnieją w zwykłym kodzie. Piszesz to tak, jakbyś napisał kopię w C ++ 03.
Z wyjątkiem teraz wektor jest przenoszony. Użytkownik klasy nie zajmuje się ona w odnośnikach rvalue w zdecydowanej większości przypadków.
źródło
tmp
nie jest przenoszony dorval_ref
, lecz zapisywany bezpośredniorval_ref
przy użyciu RVO (tzn. Elision copy). Istnieje rozróżnienie międzystd::move
elision a copy. Astd::move
może nadal wymagać kopiowania niektórych danych; w przypadku wektora nowy konstruktor jest tak naprawdę konstruowany w kreatorze kopiowania i dane są przydzielane, ale większość tablicy danych jest kopiowana tylko przez skopiowanie wskaźnika (zasadniczo). Eliminacja kopii pozwala uniknąć 100% wszystkich kopii.rval_ref
zmienne są konstruowane przy użyciu konstruktora przenoszenia zstd::vector
. Nie ma konstruktora kopii zaangażowanego zarówno z / bezstd::move
. w tym przypadkutmp
jest traktowany jako wartość wreturn
instrukcji.Prosta odpowiedź brzmi: powinieneś pisać kod dla wartości referencyjnych, tak jak normalny kod referencyjny, i powinieneś traktować je tak samo mentalnie przez 99% czasu. Obejmuje to wszystkie stare reguły dotyczące zwracania referencji (tj. Nigdy nie zwracaj referencji do zmiennej lokalnej).
O ile nie piszesz szablonowej klasy kontenera, która musi skorzystać ze std :: forward i być w stanie napisać ogólną funkcję, która przyjmuje odwołania do wartości lub wartości, jest to mniej więcej prawda.
Jedną z dużych zalet konstruktora przenoszenia i przypisania przeniesienia jest to, że jeśli je zdefiniujesz, kompilator może ich użyć w przypadkach, gdy RVO (optymalizacja wartości zwracanej) i NRVO (optymalizacja nazwanej wartości zwrotnej) nie zostaną wywołane. Jest to dość duże, jeśli chodzi o wydajne zwracanie drogich obiektów, takich jak kontenery i łańcuchy, pod względem wartości z metod.
Teraz, gdy sprawy stają się interesujące w odniesieniu do odwołań do wartości, można również użyć ich jako argumentów normalnych funkcji. Pozwala to na pisanie kontenerów, które mają przeciążenia zarówno dla stałej referencji (const foo i inne), jak i wartości referencyjnej (foo i& inne). Nawet jeśli argument jest zbyt niewygodny, aby przekazać go zwykłym wywołaniem konstruktora, nadal można to zrobić:
Kontenery STL zostały zaktualizowane w taki sposób, że mają przeciążenia związane z przenoszeniem prawie wszystkiego (klucz skrótu i wartości, wstawianie wektorów itp.) I tam, gdzie będzie ich najwięcej.
Możesz także użyć ich do normalnych funkcji, a jeśli podasz tylko argument odwołania do wartości, możesz zmusić program wywołujący do utworzenia obiektu i pozwolić funkcji wykonać ruch. Jest to raczej przykład niż naprawdę dobre zastosowanie, ale w mojej bibliotece renderowania przypisałem ciąg do wszystkich załadowanych zasobów, aby łatwiej było zobaczyć, co każdy obiekt reprezentuje w debuggerze. Interfejs wygląda mniej więcej tak:
Jest to forma „nieszczelnej abstrakcji”, ale pozwala mi skorzystać z faktu, że musiałem tworzyć ciąg już przez większość czasu i uniknąć tworzenia kolejnego kopiowania. Nie jest to dokładnie kod o wysokiej wydajności, ale jest dobrym przykładem możliwości, gdy ludzie znają tę funkcję. Ten kod faktycznie wymaga, aby zmienna była tymczasowa dla wywołania lub wywołana std :: move:
lub
lub
ale to się nie skompiluje!
źródło
Nie odpowiedź sama w sobie , ale wskazówka. W większości przypadków deklarowanie
T&&
zmiennej lokalnej nie ma większego sensu (jak w przypadkustd::vector<int>&& rval_ref
). Nadal będziesz musiałstd::move()
je użyć wfoo(T&&)
metodach typu. Istnieje również problem, o którym już wspomniano, że gdy spróbujesz powrócićrval_ref
z funkcji, otrzymasz standardowe odwołanie do zniszczonego tymczasowego fiasku.Najczęściej wybieram następujący wzór:
Nie przechowuje się żadnych referencji do zwróconych obiektów tymczasowych, dzięki czemu unika się (niedoświadczonego) błędu programisty, który chce użyć przeniesionego obiektu.
Oczywiście są (choć raczej rzadkie) przypadki, w których funkcja naprawdę zwraca wartość,
T&&
która jest odniesieniem do nietrwałego obiektu, który można przenieść do swojego obiektu.Odnośnie RVO: mechanizmy te ogólnie działają, a kompilator może ładnie uniknąć kopiowania, ale w przypadkach, gdy ścieżka zwrotna nie jest oczywista (wyjątki,
if
warunki określające nazwany obiekt, który zwrócisz i prawdopodobnie kilka innych), odnośniki są twoimi wybawcami (nawet jeśli potencjalnie więcej kosztowny).źródło
Żadne z nich nie wykona żadnego dodatkowego kopiowania. Nawet jeśli RVO nie jest używane, nowy standard mówi, że konstrukcja przenoszenia jest preferowana do kopiowania podczas dokonywania zwrotów.
Wierzę, że twój drugi przykład powoduje niezdefiniowane zachowanie, ponieważ zwracasz odwołanie do zmiennej lokalnej.
źródło
Jak już wspomniano w komentarzach do pierwszej odpowiedzi,
return std::move(...);
konstrukcja może mieć znaczenie w przypadkach innych niż zwracanie zmiennych lokalnych. Oto działający przykład, który dokumentuje, co dzieje się, gdy zwracasz obiekt członkowski zi bezstd::move()
:Przypuszczalnie
return std::move(some_member);
ma to sens tylko wtedy, gdy rzeczywiście chcesz przenieść konkretnego członka klasy, np. W przypadku, gdyclass C
reprezentuje on krótkotrwałe obiekty adaptera wyłącznie w celu utworzenia instancji klasystruct A
.Wskazówki jak
struct A
zawsze zostanie skopiowane zclass B
, nawet wtedy, gdyclass B
obiekt jest wartość R. Wynika to z faktu, że kompilator nie ma sposobu na stwierdzenie, żeclass B
jego instancjastruct A
nie będzie już używana. Wclass C
kompilator ma tę informacjęstd::move()
, dlategostruct A
zostaje przeniesiony , chyba że instancjaclass C
jest stała.źródło