- http://thbecker.net/articles/rvalue_references/section_01.html
- http://channel9.msdn.com/Shows/Going+Deep/Cpp-and-Beyond-2012-Scott-Meyers-Universal-References-in-Cpp11
- http://en.cppreference.com/w/cpp/language/reference
- http://isocpp.org/blog/2012/11/universal-references-in-c11-scott-meyers
rvalue reference
Najpierw definicja lvalue/rvalue, które znalazłem pod pierwszym linkiem:
lvalue jest wyrażeniem, które odnosi się do jakiegoś miejsca w pamięci i możemy uzyskać adres tego miejsca korzystając z operatora &. rvalue jest wyrażeniem, które nie jest lvalue.Można też stosować definicję "if it has a name ..." - link.
Jeżeli zmienna posiada nazwę to możemy uzyskać adres tej zmiennej, dzięki operatorowi &, wtedy wiem, że jest to lvalue. Nie możemy np. uzyskać nazwy literału (liczba 42), więc wiemy, że jest to rvalue.lvalue reference mają trwały stan, tymczasem rvalue reference są albo literałami, albo tymczasowymi obiektami utworzonymi w czasie ewaluowania wyrażenia. Mają zatem ważne właściwości [C++Primer - s.533]:
- Odnoszą się tylko do obiektów, które zostaną zaraz zniszczone
- Nie ma innych użytkowników danego obiektu
#include <iostream> using namespace std; int main() { int i = 42; // zmienna i jest lvalue int& lv1 = i; // lvalue reference - referencja na i // int& lv2 = i * 5; // i*5 to rvalue (stała) nie możemy zrobić referencji która pozwala na jej modyfikację const int& lv3 = i * 5; // lvalue reference - możemy, ustawić referencję na stałą // int&& rv1 = i; // rvalue reference - nie może pokazywać na lvalue auto&& lv4 = i; // lvalue reference - następuje dedukcja typu na "int&" int&& rv2 = 42; // rvalue reference - 42 to literał int&& rv3 = i * 3; // rvalue reference - wynik mnożenia to ulotna wartość (rvalue) // int&& rv4 = rv2; // rv2 nie jest ulotne i jest ważne do końca zakresu! int&& rv5 = std::move(rv2); // rvalue reference - obietnica, że nie będziemy korzystać z rv2! }
Reference collapsing i dedukcja typu
Logika podpowiada, że wszystko co ma postać T&& można by nazwać rvalue reference. Niestety nie jest to prawda, zależy to bowiem od kontekstu w jakim ta postać istnieje. Zgodnie z nowym standardem wszędzie tam, gdzie mamy do czynienia z dedukcją typu (szablony, auto, typedef oraz pewne konstrukcje decltype) i jest symbol &&, czasami możemy mieć rvalue reference, a czasami lvalue reference. Scott Meyers, proponuje swoją terminologię i nazywanie takich referencji universal reference, bowiem taka referencja może pokazywać na "wszystko": lvalue, rvalue, const, non-const, ...
Kiedy parametr funkcji jest typu T&&, a dodatkowo mamy do czynienia z dedukcją typu (bo funkcja jest szablonowa), to typ może ulec zmianie. Standard przewiduje trzy wyjątki (w książce dwa).
- Dla argumentu będącego lvalue => T będzie lvalue referencją (czyli T&)
- Dla argumentu będącego rvalue => T będzie po prostu T - o tej zasadzie wspomina Meyers w swoim wykładzie [strona 26], nie znalazłem tego w książce.
- Jeśli stworzymy referencję do referencji (choć nie bezpośrednio) wtedy ta referencja się "zapada". rvalue reference zapada się do rvalue reference, w pozostałych przypadkach zapadanie jest do lvalue reference.
- T& & => T&
- T& && => T&
- T&& & => T&
- T&& && => T&&
Przykłady:
#include <iostream> // T&& to universal reference template <typename T> void f(T&& t, std::string name, std::string descr) { std::cout << descr << std::endl; if (std::is_const<typename std::remove_reference<decltype(t)>::type>::value) std::cout << name << " is const" << std::endl; if (std::is_lvalue_reference<decltype(t)>::value) std::cout << name << " is lvalue reference" << std::endl; if (std::is_rvalue_reference<decltype(t)>::value) std::cout << name << " is rvalue reference" << std::endl; std::cout << std::endl; } int main() { f(42, "42", "f<int>(int &&) => f<int>(int&&)"); int v1 = 42; // lvalue f(v1, "v1", "f<int&>(int& &&) => f<int&>(int&)"); const int v2 = 42; // const lvalue f(v2, "v2", "f<const int&>(const int& &&) => f<const int&>(const int&)"); int tmp = 42; // lvalue int& v3 = tmp; // lvalue reference f(v3, "v3", "f<int&>(int& &&) => f<int&>(int&)"); const int& v4 = 42; // lvalue reference na const f(v4, "v4", "f<const int&>(const in& &&) => f<const int&>(const int&)"); int&& v5 = 42; // rvalue reference if (std::is_rvalue_reference<decltype(v5)>::value) std::cout << "in main v5 if rvalue reference" << std::endl; f(v5, "v5", "f<int&>(int& &&) => f<int&>(int&)"); const int&& v6 = 42; // rvalue reference na const if (std::is_rvalue_reference<decltype(v6)>::value) std::cout << "in main v6 if rvalue reference" << std::endl; f(v6, "v6", "f<const int&>(const int& &&) => f<const int&>(const int&)"); int v7 = 67; f(std::move(v7), "v7", "f<int>(int &&) => f<int>(int&&)"); }Chociaż v5 i v6 to rvalue referencje, dzięki nazwie możemy uzyskać ich adres, więc w środku funkcji f(), będą to już lvalue referencje. Aby przekazać rvalue referencję do środka, trzeba skorzystać z std::move. Wynik:
f<int>(int &&) => f<int>(int&&) 42 is rvalue reference f<int&>(int& &&) => f<int&>(int&) v1 is lvalue reference f<const int&>(const int& &&) => f<const int&>(const int&) v2 is const v2 is lvalue reference f<int&>(int& &&) => f<int&>(int&) v3 is lvalue reference f<const int&>(const in& &&) => f<const int&>(const int&) v4 is const v4 is lvalue reference in main v5 if rvalue reference f<int&>(int& &&) => f<int&>(int&) v5 is lvalue reference in main v6 if rvalue reference f<const int&>(const int& &&) => f<const int&>(const int&) v6 is const v6 is lvalue reference f<int>(int &&) => f<int>(int&&) v7 is rvalue referenceZ czterech form, dla których może następować dedukcja typu, trzy zachowują się tak samo: typ szablonowy, auto oraz typedef. Niestety decltype, posiada pewne wyjątki od tej reguły, ale w tej chwili, nie chce mi się w to zagłębiać. Może kiedyś.
Value categories
W C++11 wraz z wprowadzeniem move semantic, liczba kategorii została rozbudowana:
- https://en.cppreference.com/w/cpp/language/value_category
- https://cpp-polska.pl/post/podzial-wyrazen-ze-wzgledu-na-kategorie-wartosci-w-c
- https://www.youtube.com/watch?v=yIOH95oIKbo
@startuml "value_categories.png" (expression) --> (glvalue) (glvalue) --> (lvalue) (glvalue) --> (xvalue) (expression) --> (rvalue) (rvalue) --> (xvalue) (rvalue) --> (prvalue) @enduml
Ich właściwości można podsumować w ten sposób:
- glvalue ("generalized" lvalue)
- mają nazwę
- rvalue (nazwa historyczna, bo mogły się pojawić po prawej stronie operatora przypisania)
- można z nich przenosić
- lvalue (nazywa historyczna, bo mogły się pojawić po lewej stronie operatora przypisania)
- mają nazwę i można z nich przenosić
- xvalue ("eXpiring" value)
- mają nazwę i można z nich przenosić
- prvalue (pure rvalue)
- nie mają nazwy i można z nich przenosić
- wywołanie funkcji, której wynikiem jest rvalue reference (np. std::move(x) zwraca rvalue reference do x)
- a[n], wyrażenie, gdzie a jest tablicą rvalue
- a.m, wyrażenie, gdzie a jest rvalue i m jest składową non-static typu non-reference
- wyrażenie rzutowania do rvalue reference (np. static_cast<char&&>(x))
Brak komentarzy:
Prześlij komentarz