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:
class cannot_benefit_from_move_semantics
{
int a; // moving an int means copying an int
float b; // moving a float means copying a float
double c; // moving a double means copying a double
char d[64]; // moving a char array means copying a char array
// ...
};
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 przez std::unique_ptr
C ++ 11. Pośredni programiści C ++ są prawdopodobnie przynajmniej w pewnym stopniu zaznajomieni std::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ś zaznajomiony auto_ptr
, jego celem jest zagwarantowanie, że dynamicznie przydzielany obiekt jest zawsze zwalniany, nawet w obliczu wyjątków:
{
std::auto_ptr<Shape> a(new Triangle);
// ...
// arbitrary code, could throw exceptions
// ...
} // <--- when a goes out of scope, the triangle is deleted automatically
Niezwykłe auto_ptr
jest to, że „kopiuje”:
auto_ptr<Shape> a(new Triangle);
+---------------+
| triangle data |
+---------------+
^
|
|
|
+-----|---+
| +-|-+ |
a | p | | | |
| +---+ |
+---------+
auto_ptr<Shape> b(a);
+---------------+
| triangle data |
+---------------+
^
|
+----------------------+
|
+---------+ +-----|---+
| +---+ | | +-|-+ |
a | p | | | b | p | | | |
| +---+ | | +---+ |
+---------+ +---------+
Uwaga, w jaki sposób inicjalizacji b
ze a
ma nie kopiować trójkąt, lecz przenosi własności trójkąta od a
celu b
. My również powiedzieć „ a
jest przeniesiony do b
” lub „trójkąt jest przeniesiony z a
celu b
”. Może się to wydawać mylące, ponieważ sam trójkąt zawsze pozostaje w tym samym miejscu w pamięci.
Przeniesienie obiektu oznacza przeniesienie własności niektórych zasobów, którymi zarządza, na inny obiekt.
Konstruktor kopii auto_ptr
prawdopodobnie wygląda mniej więcej tak (nieco uproszczony):
auto_ptr(auto_ptr& source) // note the missing const
{
p = source.p;
source.p = 0; // now the source no longer owns the object
}
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 przeniesienia auto_ptr
spowoduje wywołanie niezdefiniowanego zachowania, dlatego należy bardzo uważać, aby nie używać tej funkcji auto_ptr
po przeniesieniu z:
auto_ptr<Shape> a(new Triangle); // create triangle
auto_ptr<Shape> b(a); // move a into b
double area = a->area(); // undefined behavior
Ale auto_ptr
nie zawsze jest niebezpieczne. Funkcje fabryczne są doskonałym rozwiązaniem dla auto_ptr
:
auto_ptr<Shape> make_triangle()
{
return auto_ptr<Shape>(new Triangle);
}
auto_ptr<Shape> c(make_triangle()); // move temporary into c
double area = make_triangle()->area(); // perfectly safe
Zwróć uwagę, że oba przykłady stosują ten sam wzorzec składniowy:
auto_ptr<Shape> variable(expression);
double area = expression->area();
A jednak jeden z nich przywołuje niezdefiniowane zachowanie, podczas gdy drugi nie. Jaka jest różnica między wyrażeniami a
a make_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ącym auto_ptr
zmienną, a wyrażeniem make_triangle()
oznaczającym wywołanie funkcji zwracającej auto_ptr
wartość, tworząc w ten sposób świeży auto_ptr
obiekt tymczasowy przy każdym wywołaniu. a
jest przykładem lwartości , natomiast make_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 przez a
wywołanie niezdefiniowanego zachowania. Z drugiej strony przejście z wartości rvalu, takich jak make_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 napiszemy make_triangle()
ponownie, otrzymamy inny tymczasowy. W rzeczywistości przeniesiony z tymczasowego już nie ma w następnym wierszu:
auto_ptr<Shape> c(make_triangle());
^ the moved-from temporary dies right here
Zwróć uwagę, że litery l
i r
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).
Wartość typu klasy jest wyrażeniem, którego ocena tworzy obiekt tymczasowy. W normalnych okolicznościach żadne inne wyrażenie w tym samym zakresie nie oznacza tego samego obiektu tymczasowego.
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 odniesienie X&
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. Z X
jakimi rodzajami wyrażeń typu mogą się wiązać?
lvalue const lvalue rvalue const rvalue
---------------------------------------------------------
X& yes
const X& yes yes yes yes
X&& yes
const X&& yes yes
W praktyce możesz zapomnieć const X&&
. Ograniczenie do czytania z wartości jest mało przydatne.
Odwołanie do wartości X&&
jest nowym rodzajem odniesienia, które wiąże się tylko z wartościami.
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 typu Y
, pod warunkiem, że następuje niejawna konwersja z Y
na X
. W takim przypadku X
tworzony jest tymczasowy typ , a odwołanie do wartości jest powiązane z tym tymczasowym:
void some_function(std::string&& r);
some_function("hello world");
W powyższym przykładzie "hello world"
jest wartością typu const char[12]
. Ponieważ istnieje niejawna konwersja od const char[12]
do const char*
do std::string
, std::string
tworzony jest typ tymczasowy , który r
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 ruchu X::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:
template<typename T>
class unique_ptr
{
T* ptr;
public:
T* operator->() const
{
return ptr;
}
T& operator*() const
{
return *ptr;
}
Konstruktor przejmuje własność obiektu, a destruktor usuwa go:
explicit unique_ptr(T* p = nullptr)
{
ptr = p;
}
~unique_ptr()
{
delete ptr;
}
Teraz nadchodzi interesująca część, konstruktor ruchu:
unique_ptr(unique_ptr&& source) // note the rvalue reference
{
ptr = source.ptr;
source.ptr = nullptr;
}
Ten konstruktor ruchu wykonuje dokładnie to samo, co auto_ptr
konstruktor kopiowania, ale może być dostarczony tylko z wartościami rvalues:
unique_ptr<Shape> a(new Triangle);
unique_ptr<Shape> b(a); // error
unique_ptr<Shape> c(make_triangle()); // okay
Druga linia się nie kompiluje, ponieważ a
jest to wartość, ale parametr unique_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 na c
. Znowu tego właśnie chcieliśmy.
Konstruktor przenoszenia przenosi własność zarządzanego zasobu na bieżący obiekt.
Przenieś operatory przypisania
Ostatnim brakującym elementem jest operator przypisania ruchu. Jego zadaniem jest zwolnienie starego zasobu i pozyskanie nowego zasobu z argumentu:
unique_ptr& operator=(unique_ptr&& source) // note the rvalue reference
{
if (this != &source) // beware of self-assignment
{
delete ptr; // release the old resource
ptr = source.ptr; // acquire the new resource
source.ptr = nullptr;
}
return *this;
}
};
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:
unique_ptr& operator=(unique_ptr source) // note the missing reference
{
std::swap(ptr, source.ptr);
return *this;
}
};
Teraz source
jest to zmienna typu unique_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ący operator=
, source
wychodzi poza zakres, automatycznie zwalniając stary zasób.
Operator przypisania przeniesienia przenosi własność zarządzanego zasobu na bieżący obiekt, zwalniając stary zasób. Idiom move-and-swap upraszcza implementację.
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ć nazwana std::cast_to_rvalue
lub std::enable_move
, ale utknęliśmy już z tym imieniem.
Oto, w jaki sposób wyraźnie przechodzisz od wartości:
unique_ptr<Shape> a(new Triangle);
unique_ptr<Shape> b(a); // still an error
unique_ptr<Shape> c(std::move(a)); // okay
Zauważ, że po trzeciej linii a
nie ma już trójkąta. To jest w porządku, ponieważ wyraźnie pisząc std::move(a)
, jasno wyraziliśmy nasze intencje: „Drogi konstruktorze, zrób wszystko, co chcesz a
, aby zainicjować c
; Już mnie to nie obchodzi a
. Nie krępuj się a
.”
std::move(some_lvalue)
rzuca wartość na wartość, umożliwiając w ten sposób kolejny ruch.
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:
expressions
/ \
/ \
/ \
glvalues rvalues
/ \ / \
/ \ / \
/ \ / \
lvalues xvalues prvalues
Zauważ, że tylko wartości x są naprawdę nowe; reszta wynika właśnie z zmiany nazwy i grupowania.
Wartości C ++ 98 są znane jako prvalues w C ++ 11. Mentalnie zamień wszystkie wystąpienia „wartości” w poprzednich akapitach na „wartość”.
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:
unique_ptr<Shape> make_triangle()
{
return unique_ptr<Shape>(new Triangle);
} \-----------------------------/
|
| temporary is moved into c
|
v
unique_ptr<Shape> c(make_triangle());
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:
unique_ptr<Shape> make_square()
{
unique_ptr<Shape> result(new Square);
return result; // note the missing std::move
}
Dlaczego konstruktor ruchów przyjmuje wartość result
jako argument? Zasięg zbliża result
się do końca i zostanie on zniszczony podczas rozwijania stosu. Nikt później nie mógł narzekać, że result
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 pisania std::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).
Nigdy nie używaj std::move
do przenoszenia automatycznych obiektów z funkcji.
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:
unique_ptr<Shape>&& flawed_attempt() // DO NOT DO THIS!
{
unique_ptr<Shape> very_bad_idea(new Square);
return std::move(very_bad_idea); // WRONG!
}
Nigdy nie zwracaj automatycznych obiektów przez odwołanie do wartości. Przenoszenie jest wykonywane wyłącznie przez konstruktora ruchów std::move
, a nie przez zwykłe powiązanie wartości z odwołaniem do wartości.
Przeprowadzka do członków
Wcześniej czy później napiszesz taki kod:
class Foo
{
unique_ptr<Shape> member;
public:
Foo(unique_ptr<Shape>&& parameter)
: member(parameter) // error
{}
};
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ście parameter
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.
Nazwane odwołanie do wartości jest wartością, podobnie jak każda inna zmienna.
Rozwiązaniem jest ręczne włączenie przenoszenia:
class Foo
{
unique_ptr<Shape> member;
public:
Foo(unique_ptr<Shape>&& parameter)
: member(std::move(parameter)) // note the std::move
{}
};
Można argumentować, że parameter
nie jest już używany po inicjalizacji member
. 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 po return
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.
X::X(const X&); // copy constructor
X& X::operator=(const X&); // copy assignment operator
X::~X(); // destructor
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.
X::X(X&&); // move constructor
X& X::operator=(X&&); // move assignment operator
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?
Jeśli napiszesz klasę bez niezarządzanych zasobów, nie musisz samodzielnie deklarować żadnej z pięciu specjalnych funkcji składowych, a otrzymasz poprawną semantykę kopiowania i przeniesiesz semantykę za darmo. W przeciwnym razie będziesz musiał samodzielnie wdrożyć specjalne funkcje składowe. Oczywiście, jeśli twoja klasa nie korzysta z semantyki ruchu, nie ma potrzeby implementowania specjalnych operacji przenoszenia.
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:
X& X::operator=(X source) // unified assignment operator
{
swap(source); // see my first answer for an explanation
return *this;
}
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:
template<typename T>
void foo(T&&);
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:
foo(make_triangle()); // T is unique_ptr<Shape>, T&& is unique_ptr<Shape>&&
unique_ptr<Shape> a(new Triangle);
foo(a); // T is unique_ptr<Shape>&, T&& is unique_ptr<Shape>&
Jeśli argumentem jest wartość typu X
, T
należy się spodziewać X
, że T&&
oznacza to X&&
. Tego się wszyscy spodziewają. Ale jeśli argumentem jest wartość typu X
, ze względu na specjalną regułę, T
należy się spodziewać X&
, a więc T&&
oznaczałoby coś takiego X& &&
. Ale ponieważ C ++ nadal nie ma pojęcia odniesień do odniesień, typ X& &&
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).
T&& nie jest referencją do wartości, ale referencją do przekazania. Wiąże się to również z wartościami lvalu, w którym to przypadku T
i T&&
są obydwoma wartościami referencyjnymi.
Jeśli chcesz ograniczyć szablon funkcji do wartości, możesz połączyć SFINAE z cechami typu:
#include <type_traits>
template<typename T>
typename std::enable_if<std::is_rvalue_reference<T&&>::value, void>::type
foo(T&&);
Realizacja ruchu
Teraz, gdy rozumiesz zwijanie referencji, oto jak std::move
można je zaimplementować:
template<typename T>
typename std::remove_reference<T>::type&&
move(T&& t)
{
return static_cast<typename std::remove_reference<T>::type&&>(t);
}
Jak widać, move
akceptuje dowolny rodzaj parametru dzięki referencji przekazywania T&&
i zwraca referencję wartości. std::remove_reference<T>::type
Wezwanie meta-funkcja jest konieczne, ponieważ w przeciwnym razie, dla lwartościami typu X
, rodzaju powrót byłby X& &&
, 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;)
Wywołanie funkcji, która zwraca odwołanie do wartości, takie jak std::move
, jest 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.