Właśnie skończyłem słuchać wywiadu radiowego podcastu Software Engineering ze Scottem Meyersem na temat C ++ 0x . Większość nowych funkcji ma dla mnie sens i jestem podekscytowany C ++ 0x, z wyjątkiem jednej. Nadal nie dostaję semantyki ruchu ... Co to dokładnie jest?
c++
c++-faq
c++11
move-semantics
dicroce
źródło
źródło
Odpowiedzi:
Najłatwiej jest mi zrozumieć semantykę przenoszenia z przykładowym kodem. Zacznijmy od bardzo prostej klasy łańcuchowej, która zawiera tylko wskaźnik do bloku pamięci przydzielonego na stos:
Ponieważ sami postanowiliśmy zarządzać pamięcią, musimy przestrzegać zasady trzech . Mam zamiar odłożyć pisanie operatora przypisania i zaimplementować na razie tylko destruktor i konstruktor kopiowania:
Konstruktor kopiowania definiuje, co to znaczy kopiować obiekty łańcuchowe. Ten parametr
const string& that
wiąże się ze wszystkimi wyrażeniami typu string, co pozwala na tworzenie kopii w następujących przykładach:Teraz pojawia się kluczowy wgląd w semantykę ruchu. Zauważ, że tylko w pierwszym wierszu, w którym kopiujemy,
x
ta głęboka kopia jest naprawdę konieczna, ponieważ możemy chcieć sprawdzićx
później i bylibyśmy bardzo zaskoczeni, gdybyx
się jakoś zmieniła. Czy zauważyłeś, jak powiedziałemx
trzy razy (cztery razy, jeśli załączysz to zdanie) i oznaczało dokładnie ten sam obiekt za każdym razem? Wyrażenia takie jakx
„wartości lv”.Argumenty w wierszach 2 i 3 nie są wartościami wartościowymi, lecz wartościami wartościowymi, ponieważ leżące u ich podstaw obiekty łańcuchowe nie mają nazw, więc klient nie ma możliwości ich ponownego sprawdzenia w późniejszym czasie. wartości r oznaczają wartości tymczasowe, które są niszczone w następnym średniku (a ściślej: na końcu pełnego wyrażenia, które leksykalnie zawiera wartość rvalue). Jest to ważne, ponieważ podczas inicjalizacji
b
ic
mogliśmy zrobić wszystko, co chcieliśmy z łańcuchem źródłowym, a klient nie mógł powiedzieć różnicy !C ++ 0x wprowadza nowy mechanizm o nazwie „odwołanie do wartości”, który między innymi pozwala nam wykryć argumenty wartości poprzez przeciążenie funkcji. Wszystko, co musimy zrobić, to napisać konstruktor z parametrem odniesienia wartości. Wewnątrz tego konstruktora możemy zrobić wszystko, co chcemy ze źródłem, o ile pozostawimy go w pewnym prawidłowym stanie:
Co my tu zrobiliśmy? Zamiast dogłębnego kopiowania danych sterty, właśnie skopiowaliśmy wskaźnik, a następnie ustawiliśmy oryginalny wskaźnik na null (aby zapobiec „delete []” z destruktora obiektu źródłowego przed zwolnieniem naszych „właśnie skradzionych danych”). W rezultacie „ukradliśmy” dane, które pierwotnie należały do ciągu źródłowego. Ponownie kluczową kwestią jest to, że pod żadnym pozorem klient nie mógł wykryć, że źródło zostało zmodyfikowane. Ponieważ tak naprawdę nie robimy tutaj kopii, nazywamy ten konstruktor „konstruktorem ruchu”. Jego zadaniem jest przenoszenie zasobów z jednego obiektu do drugiego zamiast ich kopiowania.
Gratulacje, teraz rozumiesz podstawy semantyki ruchów! Kontynuujmy, wdrażając operatora przypisania. Jeśli nie znasz idiomu kopiowania i zamiany , naucz się go i wróć, ponieważ jest to niesamowity idiom C ++ związany z bezpieczeństwem wyjątków.
Co to jest? „Gdzie jest odniesienie do wartości?” możesz zapytać. „Nie potrzebujemy tego tutaj!” to moja odpowiedź :)
Zauważ, że przekazujemy parametr
that
przez wartość , więcthat
musi być inicjowany tak jak każdy inny obiekt łańcucha. Dokładnie w jaki sposóbthat
zostanie zainicjowany? W dawnych czasach C ++ 98 odpowiedzią byłby „konstruktor kopii”. W C ++ 0x kompilator wybiera między konstruktorem kopiującym a konstruktorem przenoszenia na podstawie tego, czy argumentem operatora przypisania jest wartość lvalue czy rvalue.Więc jeśli powiesz
a = b
, konstruktor kopiowania zostanie zainicjowanythat
(ponieważ wyrażenieb
jest wartością), a operator przypisania zamieni zawartość z świeżo utworzoną, głęboką kopią. To jest właśnie definicja kopiowania i zamiany idiomu - zrób kopię, zamień zawartość z kopią, a następnie pozbądź się kopii, opuszczając zakres. Nic nowego tutaj.Ale jeśli powiesz
a = x + y
, konstruktor ruchu zostanie zainicjowanythat
(ponieważ wyrażeniex + y
jest wartością), więc nie ma w nim głębokiej kopii, tylko efektywny ruch.that
jest nadal niezależnym obiektem od argumentu, ale jego konstrukcja była trywialna, ponieważ dane sterty nie musiały być kopiowane, tylko przenoszone. Nie trzeba go było kopiować, ponieważx + y
jest to wartość, i znowu można przesuwać się od obiektów ciągów oznaczonych wartościami rvalues.Podsumowując, konstruktor kopii tworzy głęboką kopię, ponieważ źródło musi pozostać nietknięte. Z drugiej strony konstruktor ruchu może po prostu skopiować wskaźnik, a następnie ustawić wskaźnik w źródle na null. W ten sposób można „zerować” obiekt źródłowy, ponieważ klient nie ma możliwości ponownej inspekcji obiektu.
Mam nadzieję, że ten przykład zdobył główny punkt. Jest o wiele więcej do wartościowania referencji i przenoszenia semantyki, które celowo pominąłem, aby było to proste. Jeśli chcesz uzyskać więcej informacji, zobacz moją dodatkową odpowiedź .
źródło
that.data = 0
tego postacie zostałyby zniszczone o wiele za wcześnie (kiedy umiera tymczasowy), a także dwukrotnie. Chcesz ukraść dane, a nie je udostępniać!delete[]
na nullptr jest zdefiniowany przez standard C ++ jako no-op.Moją pierwszą odpowiedzią było bardzo uproszczone wprowadzenie do semantyki przenoszenia, a wiele szczegółów zostało pominiętych, aby uprościć to. Jednak semantyka wymaga znacznie więcej i pomyślałem, że nadszedł czas na drugą odpowiedź, aby wypełnić luki. Pierwsza odpowiedź jest już dość stara i nie wystarczyło zastąpić ją zupełnie innym tekstem. Myślę, że nadal dobrze służy jako pierwsze wprowadzenie. Ale jeśli chcesz kopać głębiej, czytaj dalej :)
Stephan T. Lavavej poświęcił czas na przekazanie cennych informacji zwrotnych. Dziękuję bardzo, Stephan!
Wprowadzenie
Przenieś semantykę pozwala obiektowi, pod pewnymi warunkami, przejąć na własność zasoby zewnętrzne innego obiektu. Jest to ważne na dwa sposoby:
Przekształcanie drogich kopii w tanie ruchy. Zobacz moją pierwszą odpowiedź na przykład. Należy zauważyć, że jeśli obiekt nie zarządza co najmniej jednym zasobem zewnętrznym (bezpośrednio lub pośrednio poprzez swoje obiekty składowe), semantyka przenoszenia nie będzie oferować żadnych korzyści w porównaniu z semantyką kopiowania. W takim przypadku kopiowanie obiektu i przenoszenie obiektu oznacza dokładnie to samo:
Wdrażanie bezpiecznych typów „tylko ruch”; to znaczy typy, dla których kopiowanie nie ma sensu, ale przenosi. Przykłady obejmują zamki, uchwyty plików i inteligentne wskaźniki z unikalną semantyką własności. Uwaga: W tej odpowiedzi omówiono
std::auto_ptr
przestarzały szablon biblioteki standardowej C ++ 98, który został zastąpiony przezstd::unique_ptr
C ++ 11. Pośredni programiści C ++ są prawdopodobnie przynajmniej w pewnym stopniu zaznajomienistd::auto_ptr
, a ze względu na wyświetlaną „semantykę ruchu” wydaje się to dobrym punktem wyjścia do omawiania semantyki ruchu w C ++ 11. YMMV.Co to jest ruch?
Standardowa biblioteka C ++ 98 oferuje inteligentny wskaźnik o unikalnej semantyce własności, tzw
std::auto_ptr<T>
. Jeśli nie jesteś zaznajomionyauto_ptr
, jego celem jest zagwarantowanie, że dynamicznie przydzielany obiekt jest zawsze zwalniany, nawet w obliczu wyjątków:Niezwykłe
auto_ptr
jest to, że „kopiuje”:Uwaga, w jaki sposób inicjalizacji
b
zea
ma nie kopiować trójkąt, lecz przenosi własności trójkąta oda
celub
. My również powiedzieć „a
jest przeniesiony dob
” lub „trójkąt jest przeniesiony za
celub
”. Może się to wydawać mylące, ponieważ sam trójkąt zawsze pozostaje w tym samym miejscu w pamięci.Konstruktor kopii
auto_ptr
prawdopodobnie wygląda mniej więcej tak (nieco uproszczony):Niebezpieczne i nieszkodliwe ruchy
Niebezpieczne
auto_ptr
jest to, że to, co syntaktycznie wygląda na kopię, jest w rzeczywistości ruchem. Próba wywołania funkcji członka w przypadku przeniesieniaauto_ptr
spowoduje wywołanie niezdefiniowanego zachowania, dlatego należy bardzo uważać, aby nie używać tej funkcjiauto_ptr
po przeniesieniu z:Ale
auto_ptr
nie zawsze jest niebezpieczne. Funkcje fabryczne są doskonałym rozwiązaniem dlaauto_ptr
:Zwróć uwagę, że oba przykłady stosują ten sam wzorzec składniowy:
A jednak jeden z nich przywołuje niezdefiniowane zachowanie, podczas gdy drugi nie. Jaka jest różnica między wyrażeniami
a
amake_triangle()
? Czyż nie są tego samego typu? Rzeczywiście są, ale mają różne kategorie wartości .Kategorie wartości
Oczywiście musi istnieć głęboka różnica między wyrażeniem
a
oznaczającymauto_ptr
zmienną, a wyrażeniemmake_triangle()
oznaczającym wywołanie funkcji zwracającejauto_ptr
wartość, tworząc w ten sposób świeżyauto_ptr
obiekt tymczasowy przy każdym wywołaniu.a
jest przykładem lwartości , natomiastmake_triangle()
jest przykładem rvalue .Przejście od wartości lv, takich jak
a
jest niebezpieczne, ponieważ moglibyśmy później spróbować wywołać funkcję członka przeza
wywołanie niezdefiniowanego zachowania. Z drugiej strony przejście z wartości rvalu, takich jakmake_triangle()
jest całkowicie bezpieczne, ponieważ po wykonaniu przez konstruktora kopiowania pracy, nie możemy ponownie użyć tymczasowego. Nie ma wyrażenia oznaczającego powiedzenie tymczasowe; jeśli po prostu napiszemymake_triangle()
ponownie, otrzymamy inny tymczasowy. W rzeczywistości przeniesiony z tymczasowego już nie ma w następnym wierszu:Zwróć uwagę, że litery
l
ir
mają historyczne pochodzenie po lewej i prawej stronie zadania. Nie jest to już prawdą w C ++, ponieważ istnieją wartości lv, które nie mogą pojawić się po lewej stronie przypisania (takie jak tablice lub typy zdefiniowane przez użytkownika bez operatora przypisania), i istnieją wartości, które mogą (wszystkie wartości dla typów klas z operatorem przypisania).Referencje wartości
Rozumiemy teraz, że przejście od wartości jest potencjalnie niebezpieczne, ale przejście od wartości jest nieszkodliwe. Gdyby C ++ miał obsługę języka w celu odróżnienia argumentów lvalue od argumentów rvalue, moglibyśmy całkowicie zabronić przejścia od wartości lv, lub przynajmniej uczynić przejście od wartości lvue jawnie w miejscu wywołania, abyśmy nie przemieszczali się przypadkowo.
Odpowiedź C ++ 11 na ten problem to odwołania do wartości . Odwołanie do wartości jest nowym rodzajem odniesienia, które wiąże się tylko z wartościami, a składnia jest następująca
X&&
. Stare dobre odniesienieX&
jest teraz znane jako odwołanie do wartości . (Uwaga: nieX&&
jest to odwołanie do odwołania; w C ++ nie ma czegoś takiego).Jeśli wrzucimy
const
do miksu, mamy już cztery różne rodzaje referencji. ZX
jakimi rodzajami wyrażeń typu mogą się wiązać?W praktyce możesz zapomnieć
const X&&
. Ograniczenie do czytania z wartości jest mało przydatne.Niejawne konwersje
Odniesienia do wartości przeszły przez kilka wersji. Od wersji 2.1 odwołanie
X&&
do wartości wiąże się również ze wszystkimi kategoriami wartości innego typuY
, pod warunkiem, że następuje niejawna konwersja zY
naX
. W takim przypadkuX
tworzony jest tymczasowy typ , a odwołanie do wartości jest powiązane z tym tymczasowym:W powyższym przykładzie
"hello world"
jest wartością typuconst char[12]
. Ponieważ istnieje niejawna konwersja odconst char[12]
doconst char*
dostd::string
,std::string
tworzony jest typ tymczasowy , któryr
jest z nim związany. Jest to jeden z przypadków, w których rozróżnienie między wartościami (wyrażeniami) i tymczasowymi (obiektami) jest nieco rozmyte.Przenieś konstruktorów
Przydatnym przykładem funkcji z
X&&
parametrem jest konstruktor ruchuX::X(X&& source)
. Jego celem jest przeniesienie własności zarządzanego zasobu ze źródła do bieżącego obiektu.W C ++ 11
std::auto_ptr<T>
został zastąpiony przez,std::unique_ptr<T>
który wykorzystuje odwołania do wartości. Opracuję i omówię uproszczoną wersjęunique_ptr
. Po pierwsze, możemy upakować surowego i wskaźnik przeciążenia operatorów->
i*
tak nasza klasa czuje się jak wskaźnik:Konstruktor przejmuje własność obiektu, a destruktor usuwa go:
Teraz nadchodzi interesująca część, konstruktor ruchu:
Ten konstruktor ruchu wykonuje dokładnie to samo, co
auto_ptr
konstruktor kopiowania, ale może być dostarczony tylko z wartościami rvalues:Druga linia się nie kompiluje, ponieważ
a
jest to wartość, ale parametrunique_ptr&& source
można powiązać tylko z wartościami. Właśnie tego chcieliśmy; niebezpieczne ruchy nigdy nie powinny być niejawne. Trzecia linia kompiluje się dobrze, ponieważmake_triangle()
jest to wartość. Konstruktor przeniesienia przeniesie własność z tymczasowej nac
. Znowu tego właśnie chcieliśmy.Przenieś operatory przypisania
Ostatnim brakującym elementem jest operator przypisania ruchu. Jego zadaniem jest zwolnienie starego zasobu i pozyskanie nowego zasobu z argumentu:
Zwróć uwagę, jak ta implementacja operatora przypisania ruchu powiela logikę zarówno destruktora, jak i konstruktora ruchu. Czy znasz idiom kopiowania i zamiany? Może być również zastosowany do semantyki przenoszenia jako idiom ruchu i zamiany:
Teraz
source
jest to zmienna typuunique_ptr
, zostanie ona zainicjowana przez konstruktor ruchu; to znaczy argument zostanie przeniesiony do parametru. Argument ten nadal musi być wartością rvalue, ponieważ sam konstruktor przenoszenia ma parametr odniesienia wartości rvalue. Gdy przepływ sterowania osiągnie nawias zamykającyoperator=
,source
wychodzi poza zakres, automatycznie zwalniając stary zasób.Odejście od wartości
Czasami chcemy przejść od wartości lv. Oznacza to, że czasami chcemy, aby kompilator traktował wartość tak, jakby była wartością, aby mógł wywołać konstruktor ruchu, nawet jeśli może być potencjalnie niebezpieczny. W tym celu C ++ 11 oferuje standardowy szablon funkcji biblioteki zwany
std::move
wewnątrz nagłówka<utility>
. Ta nazwa jest trochę niefortunna, ponieważstd::move
po prostu rzuca wartość na wartość; sam niczego nie porusza. To po prostu pozwala się poruszać. Może powinna była zostać nazwanastd::cast_to_rvalue
lubstd::enable_move
, ale utknęliśmy już z tym imieniem.Oto, w jaki sposób wyraźnie przechodzisz od wartości:
Zauważ, że po trzeciej linii
a
nie ma już trójkąta. To jest w porządku, ponieważ wyraźnie piszącstd::move(a)
, jasno wyraziliśmy nasze intencje: „Drogi konstruktorze, zrób wszystko, co chcesza
, aby zainicjowaćc
; Już mnie to nie obchodzia
. Nie krępuj sięa
.”Xvalues
Zauważ, że pomimo tego, że
std::move(a)
jest to wartość, jej ocena nie tworzy obiektu tymczasowego. Ta zagadka zmusiła komitet do wprowadzenia trzeciej kategorii wartości. Coś, co można powiązać z odwołaniem do wartości, chociaż nie jest to wartość w tradycyjnym znaczeniu, nazywa się wartością x (wartość eXpiring). Tradycyjne wartości zostały przemianowane na wartości (wartości czyste).Zarówno wartości, jak i wartości x są wartościami. Xvalues i lvalues są zarówno wartościami glvalu (Uogólnione wartości lvalu). Relacje łatwiej zrozumieć za pomocą diagramu:
Zauważ, że tylko wartości x są naprawdę nowe; reszta wynika właśnie z zmiany nazwy i grupowania.
Wyprowadzanie się z funkcji
Do tej pory widzieliśmy ruch do zmiennych lokalnych i parametrów funkcji. Ale ruch jest również możliwy w przeciwnym kierunku. Jeśli funkcja zwraca wartość, jakiś obiekt w miejscu wywołania (prawdopodobnie zmienna lokalna lub tymczasowa, ale może to być dowolny obiekt) jest inicjowany wyrażeniem po
return
instrukcji jako argumentem konstruktora przenoszenia:Być może, co zaskakujące, obiekty automatyczne (zmienne lokalne, które nie są zadeklarowane jako
static
) mogą być również domyślnie usunięte z funkcji:Dlaczego konstruktor ruchów przyjmuje wartość
result
jako argument? Zasięg zbliżaresult
się do końca i zostanie on zniszczony podczas rozwijania stosu. Nikt później nie mógł narzekać, żeresult
to się jakoś zmieniło; gdy przepływ kontrolny powraca do dzwoniącego,result
już nie istnieje! Z tego powodu C ++ 11 ma specjalną regułę, która pozwala na zwracanie automatycznych obiektów z funkcji bez konieczności pisaniastd::move
. W rzeczywistości nigdy nie należy używaćstd::move
do przenoszenia automatycznych obiektów z funkcji, ponieważ hamuje to „nazwaną optymalizację wartości zwrotnej” (NRVO).Należy zauważyć, że w obu funkcjach fabrycznych typ zwracany jest wartością, a nie odniesieniem do wartości. Odwołania do wartości są nadal odwołaniami i jak zawsze, nigdy nie powinieneś zwracać odniesienia do obiektu automatycznego; program wywołujący miałby zwisające odniesienie, gdybyś nakłonił kompilator do zaakceptowania kodu, w następujący sposób:
Przeprowadzka do członków
Wcześniej czy później napiszesz taki kod:
Zasadniczo kompilator będzie narzekał, że
parameter
jest to wartość. Jeśli spojrzysz na jego typ, zobaczysz odwołanie do wartości, ale odwołanie do wartości oznacza po prostu „odwołanie powiązane z wartością”; to jednak nie znaczy, że sama odniesienia jest rvalue! Rzeczywiścieparameter
jest zwykłą zmienną o nazwie. Możesz używaćparameter
tak często, jak chcesz w ciele konstruktora i zawsze oznacza ten sam obiekt. Domniemane odejście od niego byłoby niebezpieczne, dlatego język tego zabrania.Rozwiązaniem jest ręczne włączenie przenoszenia:
Można argumentować, że
parameter
nie jest już używany po inicjalizacjimember
. Dlaczego nie ma specjalnej reguły, aby wstawiać dyskretnie,std::move
podobnie jak w przypadku zwracanych wartości? Prawdopodobnie dlatego, że byłoby to zbyt duże obciążenie dla implementatorów kompilatora. Na przykład, co jeśli ciało konstruktora znajdowało się w innej jednostce tłumaczeniowej? Natomiast reguła wartości zwracanej musi po prostu sprawdzać tabele symboli, aby ustalić, czy identyfikator poreturn
słowie kluczowym oznacza automatyczny obiekt.Możesz także przekazać
parameter
wartość. W przypadku typów typu „tylko ruch”unique_ptr
wydaje się, że nie ma jeszcze ustalonego idiomu. Osobiście wolę przekazywać według wartości, ponieważ powoduje to mniej bałaganu w interfejsie.Specjalne funkcje członka
C ++ 98 domyślnie deklaruje trzy specjalne funkcje składowe na żądanie, to znaczy, gdy są one gdzieś potrzebne: konstruktor kopii, operator przypisania kopii i destruktor.
Referencje dotyczące wartości przeszły przez kilka wersji. Od wersji 3.0 C ++ 11 deklaruje na żądanie dwie dodatkowe specjalne funkcje składowe: konstruktor ruchu i operator przypisania ruchu. Pamiętaj, że ani VC10, ani VC11 nie są jeszcze zgodne z wersją 3.0, więc będziesz musiał je zaimplementować samodzielnie.
Te dwie nowe specjalne funkcje składowe są deklarowane domyślnie tylko wtedy, gdy żadna ze specjalnych funkcji składowych nie jest zadeklarowana ręcznie. Ponadto, jeśli zadeklarujesz własnego konstruktora przeniesienia lub operatora przypisania przeniesienia, ani konstruktor kopiowania, ani operator przypisania kopiowania nie zostaną zadeklarowane pośrednio.
Co te zasady oznaczają w praktyce?
Zauważ, że operator przypisania kopii i operator przypisania przeniesienia można połączyć w jeden, zunifikowany operator przypisania, biorąc jego argument według wartości:
W ten sposób liczba specjalnych funkcji składowych do implementacji spada z pięciu do czterech. Istnieje tutaj kompromis między bezpieczeństwem wyjątkowym a wydajnością, ale nie jestem ekspertem w tej kwestii.
Przekazywanie referencji ( wcześniej znane jako Uniwersalne referencje )
Rozważ następujący szablon funkcji:
Można się spodziewać
T&&
wiązania wyłącznie wartości rvalu, ponieważ na pierwszy rzut oka wygląda jak odniesienie do wartości. Jak się jednak okazuje,T&&
wiąże się również z wartościami:Jeśli argumentem jest wartość typu
X
,T
należy się spodziewaćX
, żeT&&
oznacza toX&&
. Tego się wszyscy spodziewają. Ale jeśli argumentem jest wartość typuX
, ze względu na specjalną regułę,T
należy się spodziewaćX&
, a więcT&&
oznaczałoby coś takiegoX& &&
. Ale ponieważ C ++ nadal nie ma pojęcia odniesień do odniesień, typX& &&
jest załamał sięX&
. Na początku może się to wydawać mylące i bezużyteczne, ale zwijanie referencji jest niezbędne dla idealnego przekazywania (co nie będzie tutaj omawiane).Jeśli chcesz ograniczyć szablon funkcji do wartości, możesz połączyć SFINAE z cechami typu:
Realizacja ruchu
Teraz, gdy rozumiesz zwijanie referencji, oto jak
std::move
można je zaimplementować:Jak widać,
move
akceptuje dowolny rodzaj parametru dzięki referencji przekazywaniaT&&
i zwraca referencję wartości.std::remove_reference<T>::type
Wezwanie meta-funkcja jest konieczne, ponieważ w przeciwnym razie, dla lwartościami typuX
, rodzaju powrót byłbyX& &&
, który załamie sięX&
. Ponieważt
zawsze jest to wartość (pamiętaj, że nazwane odwołanie do wartości jest wartością), ale chcemy powiązaćt
z odwołaniem do wartości, musimy jawnie rzutowaćt
na poprawny typ zwracany. Wywołanie funkcji, która zwraca odwołanie do wartości, samo w sobie jest wartością x. Teraz już wiesz, skąd pochodzą wartości x;)Zwróć uwagę, że w tym przykładzie zwracanie przez odwołanie do wartości jest w porządku, ponieważ
t
nie oznacza obiektu automatycznego, ale obiekt przekazany przez program wywołujący.źródło
Semantyka przenoszenia oparta jest na odwołaniach do wartości .
Wartość jest obiektem tymczasowym, który zostanie zniszczony na końcu wyrażenia. W obecnym C ++ wartości rwiążą się tylko z
const
referencjami. C ++ 1x zezwoli naconst
odniesienia nie będące wartościami, pisaneT&&
, które są odniesieniami do obiektów wartości.Ponieważ wartość umrze na końcu wyrażenia, możesz ukraść jej dane . Zamiast kopiować go do innego obiektu, przenosisz do niego dane.
W powyższym kodzie, ze starymi kompilatorów wynikiem
f()
jest kopiowany dox
korzystaniaX
„s konstruktor kopiujący. Jeśli twój kompilator obsługuje semantykę ruchu iX
ma konstruktor ruchu, wówczas jest to wywoływane. Ponieważ jegorhs
argument jest wartością , wiemy, że nie jest już potrzebny i możemy ukraść jego wartość.Tak więc wartość jest przenoszona z nienazwanego tymczasowego zwracanego z
f()
dox
(podczas gdy danex
, zainicjowane na pustyX
, są przenoszone do tymczasowego, który zostanie zniszczony po przypisaniu).źródło
this->swap(std::move(rhs));
ponieważ nazwane referencje wartości są wartościami lvrhs
jest lwartość w kontekścieX::X(X&& rhs)
. Musisz zadzwonić,std::move(rhs)
żeby zdobyć wartość, ale ten rodzaj odpowiedzi jest dyskusyjny.Załóżmy, że masz funkcję, która zwraca znaczny obiekt:
Kiedy piszesz taki kod:
wtedy zwykły kompilator C ++ utworzy tymczasowy obiekt dla wyniku
multiply()
, wywoła konstruktora kopiowania w celu zainicjowaniar
, a następnie zniszczy tymczasową wartość zwracaną. Przestaw semantykę w C ++ 0x pozwala na wywołanie „konstruktora ruchu” w celu zainicjowaniar
poprzez skopiowanie jego zawartości, a następnie odrzucenie wartości tymczasowej bez konieczności jej niszczenia.Jest to szczególnie ważne, jeśli (jak być może w
Matrix
powyższym przykładzie) kopiowany obiekt przydziela dodatkową pamięć na stercie do przechowywania swojej wewnętrznej reprezentacji. Konstruktor kopii musiałby albo wykonać pełną kopię wewnętrznej reprezentacji, albo wewnętrznie korzystać z liczenia referencji i semantyki kopiowania przy zapisie. Konstruktor ruchu pozostawiłby pamięć stosu w spokoju i po prostu skopiował wskaźnik wewnątrzMatrix
obiektu.źródło
Jeśli naprawdę interesuje Cię dobre, dogłębne wyjaśnienie semantyki ruchów, bardzo polecam przeczytanie na nich oryginalnej pracy „Propozycja dodania obsługi semantyki ruchu do języka C ++”.
Jest bardzo dostępny i łatwy do odczytania, co stanowi doskonały argument za oferowanymi przez nie korzyściami. Na stronie WG21 dostępne są inne nowsze i aktualne artykuły na temat semantyki ruchów , ale ta jest prawdopodobnie najprostsza, ponieważ podchodzi do rzeczy z najwyższego poziomu i nie zagłębia się w szczegółowe szczegóły języka.
źródło
Semantyka Move polega na przesyłaniu zasobów, a nie na ich kopiowaniu, gdy nikt już nie potrzebuje wartości źródłowej.
W C ++ 03 obiekty są często kopiowane, tylko w celu zniszczenia lub przeniesienia, zanim jakikolwiek kod ponownie użyje wartości. Na przykład, gdy zwracasz wartość z funkcji - chyba że RVO włączy się - zwracana wartość jest kopiowana do ramki stosu wywołującego, a następnie wychodzi poza zakres i zostaje zniszczona. Jest to tylko jeden z wielu przykładów: patrz wartość przekazywana, gdy obiekt źródłowy jest tymczasowy, algorytmy takie
sort
po prostu przestawiają elementy, realokacja,vector
gdy jego wartośćcapacity()
zostanie przekroczona itp.Kiedy takie pary kopiuj / niszcz są kosztowne, dzieje się tak zwykle dlatego, że obiekt posiada pewne zasoby ciężkie. Na przykład
vector<string>
może posiadać dynamicznie przydzielany blok pamięci zawierający tablicęstring
obiektów, z których każdy ma własną pamięć dynamiczną. Kopiowanie takiego obiektu jest kosztowne: musisz przydzielić nową pamięć dla każdego dynamicznie przydzielonego bloku w źródle i skopiować wszystkie wartości. Następnie musisz cofnąć przydział całej pamięci, którą właśnie skopiowałeś. Jednak przeniesienie dużegovector<string>
oznacza po prostu skopiowanie kilku wskaźników (odnoszących się do dynamicznego bloku pamięci) do miejsca docelowego i wyzerowanie ich w źródle.źródło
W prostych (praktycznych) kategoriach:
Kopiowanie obiektu oznacza kopiowanie jego „statycznych” elementów i wywoływanie
new
operatora dla jego obiektów dynamicznych. Dobrze?Jednak przesunięcie obiektu (powtarzam, z praktycznego punktu widzenia) oznacza tylko skopiowanie wskaźników obiektów dynamicznych, a nie tworzenie nowych.
Ale czy to nie jest niebezpieczne? Oczywiście można dwukrotnie zniszczyć obiekt dynamiczny (błąd segmentacji). Aby tego uniknąć, należy „unieważnić” wskaźniki źródłowe, aby uniknąć ich dwukrotnego zniszczenia:
Ok, ale jeśli przesunę obiekt, obiekt źródłowy stanie się bezużyteczny, nie? Oczywiście, ale w niektórych sytuacjach jest to bardzo przydatne. Najbardziej oczywistym jest to, że wywołuję funkcję z obiektem anonimowym (obiekt tymczasowy, obiekt wartości ... możesz wywołać ją pod inną nazwą):
W tej sytuacji anonimowy obiekt jest tworzony, następnie kopiowany do parametru funkcji, a następnie usuwany. Dlatego lepiej jest przenieść obiekt, ponieważ nie potrzebujesz obiektu anonimowego i możesz zaoszczędzić czas i pamięć.
Prowadzi to do koncepcji odniesienia do „wartości”. Istnieją w C ++ 11 tylko w celu wykrycia, czy otrzymany obiekt jest anonimowy, czy nie. Myślę, że już wiesz, że „wartość” jest przypisywalną jednostką (lewa część
=
operatora), więc potrzebujesz nazwanego odwołania do obiektu, aby móc działać jako wartość. Wartość jest dokładnie odwrotna, obiekt bez nazwanych odniesień. Z tego powodu anonimowy obiekt i wartość są synonimami. Więc:W takim przypadku, gdy obiekt typu
A
powinien zostać „skopiowany”, kompilator tworzy odwołanie do wartości lub odwołanie do wartości w zależności od tego, czy przekazany obiekt ma nazwę, czy nie. Gdy nie, wywoływany jest twój konstruktor ruchów i wiesz, że obiekt jest tymczasowy, i możesz przenosić jego obiekty dynamiczne zamiast kopiować je, oszczędzając miejsce i pamięć.Należy pamiętać, że obiekty „statyczne” są zawsze kopiowane. Nie ma możliwości „przeniesienia” obiektu statycznego (obiektu na stosie, a nie na stosie). Zatem rozróżnienie „ruch” / „kopia”, gdy obiekt nie ma elementów dynamicznych (bezpośrednio lub pośrednio), jest nieistotne.
Jeśli twój obiekt jest złożony, a destruktor ma inne efekty wtórne, takie jak wywołanie funkcji biblioteki, wywołanie innych funkcji globalnych lub cokolwiek to jest, być może lepiej jest zasygnalizować ruch za pomocą flagi:
Twój kod jest więc krótszy (nie musisz wykonywać
nullptr
przypisania dla każdego członka dynamicznego) i bardziej ogólny.Inne typowe pytanie: jaka jest różnica między
A&&
iconst A&&
? Oczywiście w pierwszym przypadku możesz modyfikować obiekt, aw drugim nie, ale praktyczne znaczenie? W drugim przypadku nie można go zmodyfikować, więc nie ma możliwości unieważnienia obiektu (z wyjątkiem zmiennej flagi lub czegoś podobnego) i nie ma praktycznej różnicy w stosunku do konstruktora kopiowania.A czym jest idealne przekazywanie ? Ważne jest, aby wiedzieć, że „odwołanie do wartości” to odniesienie do nazwanego obiektu w „zakresie osoby wywołującej”. Ale w rzeczywistym zakresie odwołanie do wartości jest nazwą obiektu, więc działa jak obiekt nazwany. Jeśli przekażesz odwołanie do wartości do innej funkcji, przekazujesz nazwany obiekt, więc obiekt nie jest odbierany jak obiekt tymczasowy.
Obiekt
a
zostanie skopiowany do rzeczywistego parametruother_function
. Jeśli chcesz, aby obiekta
był nadal traktowany jako obiekt tymczasowy, powinieneś użyćstd::move
funkcji:W tym wierszu
std::move
nastąpi rzuta
na wartość iother_function
otrzyma obiekt jako obiekt bez nazwy. Oczywiście, jeśliother_function
nie ma specyficznego przeciążenia do pracy z obiektami nienazwanymi, to rozróżnienie nie jest ważne.Czy to idealne przekazywanie? Nie, ale jesteśmy bardzo blisko. Idealne przekazywanie jest przydatne tylko do pracy z szablonami, aby powiedzieć: jeśli muszę przekazać obiekt do innej funkcji, potrzebuję tego, że jeśli otrzymam nazwany obiekt, obiekt jest przekazywany jako obiekt nazwany, a gdy nie, Chcę przekazać to jak obiekt bez nazwy:
Jest to sygnatura prototypowej funkcji wykorzystującej doskonałe przekazywanie, zaimplementowanej w C ++ 11 za pomocą
std::forward
. Ta funkcja wykorzystuje niektóre reguły tworzenia szablonów:Jeśli więc
T
jest odniesieniem do wartościA
( T = A i), toa
także ( A i && => A i). JeśliT
jest to odniesienie do wartościA
,a
również (A &&&& => A&&). W obu przypadkacha
jest nazwanym obiektem w rzeczywistym zasięgu, aleT
zawiera informacje o jego „typie odniesienia” z punktu widzenia zakresu dzwoniącego. Ta informacja (T
) jest przekazywana jako parametr szablonu do,forward
a „a” jest przenoszone lub nie zgodnie z typemT
.źródło
To jak kopiowanie semantyki, ale zamiast duplikowania wszystkich danych, możesz ukraść dane z obiektu, z którego się „przenosi”.
źródło
Wiesz, co oznacza semantyka kopiowania, prawda? oznacza to, że masz typy, które można kopiować, w przypadku typów zdefiniowanych przez użytkownika możesz to zdefiniować albo kupić bezpośrednio, pisząc konstruktor i operator przypisania kopii, lub kompilator generuje je niejawnie. Spowoduje to wykonanie kopii.
Semantyka ruchu jest w zasadzie typem zdefiniowanym przez użytkownika z konstruktorem, który przyjmuje referencję wartości r (nowy typ referencji za pomocą && (tak dwa znaki ampersand)), który nie jest stały, nazywa się to konstruktorem ruchu, to samo dotyczy operatora przypisania. Więc co robi konstruktor ruchu, zamiast kopiować pamięć z argumentu źródłowego, „przenosi” pamięć ze źródła do miejsca docelowego.
Kiedy chcesz to zrobić? well std :: vector jest przykładem, powiedzmy, że utworzyłeś tymczasowy std :: vector i zwracasz go z funkcji:
Będziesz mieć narzut od konstruktora kopiowania, gdy funkcja powróci, jeśli (i będzie to w C ++ 0x) std :: vector ma konstruktor ruchu zamiast kopiowania, może po prostu ustawić wskaźniki i „przenieść” dynamicznie przydzielane pamięć do nowej instancji. To coś w rodzaju semantyki przeniesienia własności ze std :: auto_ptr.
źródło
Aby zilustrować potrzebę semantyki przenoszenia , rozważmy ten przykład bez semantyki przenoszenia:
Oto funkcja, która pobiera obiekt typu
T
i zwraca obiekt tego samego typuT
:Powyższa funkcja używa wywołania według wartości, co oznacza, że po wywołaniu tej funkcji należy zbudować obiekt, aby mogła być użyta przez funkcję.
Ponieważ funkcja zwraca także wartość , dla zwracanej wartości konstruowany jest kolejny nowy obiekt:
Zbudowano dwa nowe obiekty, z których jeden jest obiektem tymczasowym używanym tylko przez czas trwania funkcji.
Gdy nowy obiekt jest tworzony na podstawie wartości zwracanej, wywoływany jest konstruktor kopiujący, aby skopiować zawartość obiektu tymczasowego do nowego obiektu b. Po zakończeniu funkcji obiekt tymczasowy użyty w funkcji wykracza poza zakres i zostaje zniszczony.
Teraz zastanówmy się, co robi konstruktor kopii .
Najpierw należy zainicjować obiekt, a następnie skopiować wszystkie odpowiednie dane ze starego obiektu do nowego.
W zależności od klasy, być może jest to kontener z bardzo dużą ilością danych, co może oznaczać dużo czasu i zużycia pamięci
Dzięki semantyce ruchów można teraz uczynić większość tej pracy mniej nieprzyjemną, po prostu przenosząc dane, a nie kopiując.
Przenoszenie danych wymaga ponownego skojarzenia danych z nowym obiektem. I w ogóle nie ma kopii .
Dokonuje się tego poprzez
rvalue
odniesienie. Odniesienia działa całkiem dużo jak odniesienia z jedną istotną różnicą: RValue odniesienia może być przemieszczany i lwartość nie może.rvalue
lvalue
Od cppreference.com :
źródło
Piszę to, aby upewnić się, że dobrze to rozumiem.
Semantyka przenoszenia została stworzona, aby uniknąć niepotrzebnego kopiowania dużych obiektów. Bjarne Stroustrup w swojej książce „The C ++ Programming Language” wykorzystuje dwa przykłady, w których domyślnie zachodzi niepotrzebne kopiowanie: jeden, zamiana dwóch dużych obiektów i dwa, zwracanie dużego obiektu z metody.
Zamiana dwóch dużych obiektów zwykle wiąże się z kopiowaniem pierwszego obiektu do obiektu tymczasowego, kopiowaniem drugiego obiektu do pierwszego obiektu i kopiowaniem obiektu tymczasowego do drugiego obiektu. W przypadku typu wbudowanego jest to bardzo szybkie, ale w przypadku dużych obiektów te trzy kopie mogą zająć dużo czasu. „Przypisanie przeniesienia” pozwala programiście zastąpić domyślne zachowanie kopiowania i zamiast tego zamienić odniesienia do obiektów, co oznacza, że w ogóle nie ma kopiowania, a operacja zamiany jest znacznie szybsza. Przypisanie ruchu można wywołać, wywołując metodę std :: move ().
Domyślne zwrócenie obiektu z metody wymaga wykonania kopii obiektu lokalnego i powiązanych z nim danych w miejscu dostępnym dla osoby wywołującej (ponieważ obiekt lokalny nie jest dostępny dla osoby wywołującej i znika po zakończeniu metody). Gdy zwracany jest typ wbudowany, ta operacja jest bardzo szybka, ale jeśli zwracany jest duży obiekt, może to zająć dużo czasu. Konstruktor ruchu pozwala programiście nadpisać to domyślne zachowanie i zamiast tego „ponownie wykorzystać” dane sterty związane z obiektem lokalnym, wskazując obiekt zwracany do obiektu wywołującego, aby stertować dane powiązane z obiektem lokalnym. Dlatego kopiowanie nie jest wymagane.
W językach, które nie zezwalają na tworzenie obiektów lokalnych (tj. Obiektów na stosie), tego rodzaju problemy nie występują, ponieważ wszystkie obiekty są przydzielane na stercie i są zawsze dostępne przez odniesienie.
źródło
x
iy
, nie tylko można „referencje swap do obiektów” ; być może obiekty zawierają wskaźniki odwołujące się do innych danych i wskaźniki te można zamieniać, ale operatorzy ruchu nie są zobowiązani do zamiany czegokolwiek. Mogą wymazać dane z przeniesionego obiektu, zamiast zachowywać w nim dane docelowe.swap()
bez semantyki ruchu. „Przypisanie ruchu można wywołać, wywołując metodę std :: move ().” - czasem konieczne jest użyciestd::move()
- chociaż to tak naprawdę niczego nie przenosi - po prostu informuje kompilator, że argument jest ruchomy, czasamistd::forward<>()
(z referencjami przekazywania), a innym razem kompilator wie, że można przenieść wartość.Oto odpowiedź z książki Bjarne Stroustrup „The C ++ Programming Language”. Jeśli nie chcesz oglądać wideo, możesz zobaczyć poniższy tekst:
Rozważ ten fragment kodu. Powrót z operatora + polega na skopiowaniu wyniku ze zmiennej lokalnej do miejsca,
res
w którym dzwoniący może uzyskać do niej dostęp.Tak naprawdę nie chcieliśmy kopii; chcieliśmy tylko uzyskać wynik z funkcji. Musimy więc przenieść Vector zamiast go skopiować. Możemy zdefiniować konstruktor ruchu w następujący sposób:
&& oznacza „odwołanie do wartości” i jest odniesieniem, z którym możemy powiązać wartość. „wartość” ma na celu uzupełnienie „wartości”, co z grubsza oznacza „coś, co może pojawić się po lewej stronie zadania”. Zatem wartość oznacza w przybliżeniu „wartość, której nie można przypisać”, na przykład liczbę całkowitą zwracaną przez wywołanie funkcji oraz
res
zmienną lokalną w operatorze + () dla wektorów.Teraz oświadczenie
return res;
nie zostanie skopiowane!źródło