Udostępnij za pomocą


Kategorie wartości i odwołania do nich

W tym temacie przedstawiono i opisano różne kategorie wartości (i odwołania do wartości), które istnieją w języku C++:

  • glvalue
  • Lvalue
  • xlvalue
  • prvalue
  • wartość r.

Bez wątpienia słyszeliście już o lvalues i rvalues. Ale możesz nie myśleć o nich w kategoriach przedstawionych w tym temacie.

Każde wyrażenie w języku C++ daje wartość należącą do jednej z pięciu kategorii wymienionych powyżej. Istnieją aspekty języka C++ — jego obiekty i reguły — które wymagają prawidłowego zrozumienia tych kategorii wartości, a także odwołań do nich. Te aspekty obejmują pobieranie adresu wartości, kopiowanie wartości, przenoszenie wartości i przekazywanie wartości do innej funkcji. Ten temat nie omawia dogłębnie wszystkich tych aspektów, ale dostarcza podstawowych informacji dla ich solidnego zrozumienia.

Informacje w tym temacie są omówione w kontekście analizy kategorii wartości Stroustrupa przez dwie niezależne właściwości: tożsamość i przenaszalność [Stroustrup, 2013].

Wartość lvalue posiada tożsamość

Co oznacza dla wartości posiadanie tożsamości ? Jeśli masz (lub możesz uzyskać) adres pamięci wartości i użyjesz go bezpiecznie, wartość ma tożsamość. W ten sposób można zrobić więcej niż porównać zawartość wartości — można je porównać lub odróżnić według tożsamości.

lvalue posiada tożsamość. Jest to teraz kwestia tylko historycznego zainteresowania, że "l" w "lvalue" jest skrótem "left" (podobnie jak w lewej stronie przypisania). W języku C++ wartość lvalue może pojawić się po lewej stronie przypisania jako lub po prawej stronie jako. „l” w „lvalue” nie pomaga zrozumieć ani zdefiniować, czym są. Musisz tylko zrozumieć, że to, co nazywamy wartością lvalue, jest wartością, która ma tożsamość.

Przykłady wyrażeń, które są wartościami lvalue, to: nazwana zmienna lub stała; lub funkcja zwracająca odwołanie. Przykłady wyrażeń, które nie są lvalues, to: tymczasowe; lub funkcja, która zwraca wartość.

int& get_by_ref() { ... }
int get_by_val() { ... }

int main()
{
    std::vector<byte> vec{ 99, 98, 97 };
    std::vector<byte>* addr1{ &vec }; // ok: vec is an lvalue.
    int* addr2{ &get_by_ref() }; // ok: get_by_ref() is an lvalue.

    int* addr3{ &(get_by_ref() + 1) }; // Error: get_by_ref() + 1 is not an lvalue.
    int* addr4{ &get_by_val() }; // Error: get_by_val() is not an lvalue.
}

Teraz, chociaż jest to prawdziwe stwierdzenie, że lvalues posiadają tożsamość, to samo dotyczy xvalues. Dowiemy się dokładnie, czym jest xvalue w dalszej części tego tematu. Na razie należy pamiętać, że istnieje kategoria wartości o nazwie glvalue (dla "uogólnionej wartości lvalue"). Zestaw glvalues jest nadzbiorem obu wartości lvalue (znanych również jako klasycznych lvalues) i xvalues. Tak więc, podczas gdy "lvalue ma tożsamość" jest prawdziwe, kompletny zbiór elementów, które mają tożsamość, to zbiór *glvalues*, jak pokazano na tej ilustracji.

Lvalue ma swoją tożsamość

Wartość rvalue jest wymienna; wartość lvalue nie jest

Ale istnieją wartości, które nie są glvalues. Innymi słowy, istnieją wartości, dla których nie można uzyskać adresu pamięci (lub nie można polegać na tym, że jest prawidłowy). W powyższym przykładzie kodu widzieliśmy kilka takich wartości.

Brak niezawodnego adresu pamięci wydaje się wadą. Ale w rzeczywistości zaletą takiej wartości jest to, że można ją przenieść (co jest ogólnie tanie), zamiast kopiować (co jest ogólnie kosztowne). Przeniesienie wartości oznacza, że nie znajduje się już w miejscu, w którym kiedyś była. Więc próba uzyskania dostępu do niego w miejscu, w którym kiedyś była, jest czymś, czego należy unikać. Omówienie, kiedy i jak przenieść wartość, jest poza zakresem tego tematu. W tym temacie wystarczy wiedzieć, że wartość ruchoma jest znana jako rvalue (lub klasyczne rvalue).

"r" w "rvalue" jest skrótem "right" (podobnie jak w prawej stronie przypisania). Można jednak używać wartości rvalue i odwołań do rvalues poza przypisaniami. "R" w "rvalue" nie jest tym, na czym należy się skupić. Musisz tylko zrozumieć, że to, co nazywamy rvalue, to wartość, która jest wymienna.

Natomiast wartość lvalue nie jest wymienna, jak pokazano na tej ilustracji. Gdyby lvalue miało się przenieść, to przeczyłoby samej definicji lvalue. Byłby to nieoczekiwany problem dla kodu, który bardzo rozsądnie oczekiwał, że będzie mógł nadal uzyskiwać dostęp do wartości lvalue.

Wartość rvalue jest przenośna; wartość lvalue nie jest przenosna

Nie można więc przenieść wartości lvalue. Jednak jest rodzajem glvalue (zbiór rzeczy posiadających tożsamość), które można przenieść — jeśli wiesz, co robisz (w tym uważasz, aby nie uzyskiwać do niego dostępu po przeniesieniu) — a mianowicie xvalue. Ponownie omówimy ten pomysł w tym temacie, gdy przyjrzymy się pełnemu obrazowi kategorii wartości.

Odwołania do wartości Rvalue i reguły powiązań odwołań

W tej sekcji przedstawiono składnię odwołania do wartości rvalue. Będziemy musieli poczekać na kolejny temat, aby przejść do szczegółowego omówienia przesyłania i przekazywania, ale dość powiedzieć, że odniesienia rvalue są niezbędnym elementem rozwiązania tych problemów. Zanim przyjrzymy się referencjom rvalue, najpierw musimy lepiej zrozumieć T&— rzecz, którą wcześniej nazywaliśmy "referencją". To w rzeczywistości odniesienie do wartości lvalue (nie-const), które pozwala użytkownikowi na zapis do tej wartości.

template<typename T> T& get_by_lvalue_ref() { ... } // Get by lvalue (non-const) reference.
template<typename T> void set_by_lvalue_ref(T&) { ... } // Set by lvalue (non-const) reference.

Referencja lvalue może wiązać się z wartością lvalue, ale nie z wartością rvalue.

Następnie istnieją odwołania do lvalue const (T const&), które odnoszą się do obiektów, do których użytkownik odwołania nie może dokonywać zapisów (jak na przykład stała).

template<typename T> T const& get_by_lvalue_cref() { ... } // Get by lvalue const reference.
template<typename T> void set_by_lvalue_cref(T const&) { ... } // Set by lvalue const reference.

Referencja const lvalue może wiązać się z lvalue lub rvalue.

Składnia odwołania do rvalue typu T jest zapisywana jako T&&. Referencja rvalue odnosi się do wartości przenośnej — wartości, której zawartości nie musimy zachowywać po jej użyciu (na przykład wartość tymczasowa). Głównym celem jest przejście (przez co modyfikujemy) wartość związaną z referencją rvalue, kwalifikatory const i volatile (znane również jako kwalifikatory cv) nie mają zastosowania do referencji rvalue.

template<typename T> T&& get_by_rvalue_ref() { ... } // Get by rvalue reference.
struct A { A(A&& other) { ... } }; // A move constructor takes an rvalue reference.

Referencja rvalue wiąże się z rvalue. W rzeczywistości, jeśli chodzi o rozpoznawanie przeciążeń, rvalue woli, aby było powiązane z odwołaniem rvalue niż z odwołaniem const lvalue. Ale odwołanie rvalue nie może wiązać się z wartością lvalue, ponieważ, jak mówiliśmy, odwołanie rvalue odnosi się do wartości, której zawartość zakłada się, że nie musimy zachowywać (powiedzmy parametr dla konstruktora przenoszenia).

Można również przekazać wartość rvalue, w której oczekiwano argumentu by-value, za pomocą konstrukcji kopiowania (lub za pomocą budowy przenoszenia, jeśli rvalue jest wartością xvalue).

Wartość glvalue ma tożsamość; wartość prvalue jej nie ma.

Na tym etapie wiemy, co stanowi tożsamość. I wiemy, co jest ruchome i co nie jest. Ale nie nazwaliśmy jeszcze zestawu wartości, które nie posiadają tożsamości. Ten zestaw jest znany jako prvaluelub pure rvalue.

int& get_by_ref() { ... }
int get_by_val() { ... }

int main()
{
    int* addr3{ &(get_by_ref() + 1) }; // Error: get_by_ref() + 1 is a prvalue.
    int* addr4{ &get_by_val() }; // Error: get_by_val() is a prvalue.
}

Wartość gl ma tożsamość; wartość pr nie ma

Pełny obraz kategorii wartości

Pozostaje tylko połączenie powyższych informacji i ilustracji w jeden, duży obraz.

Pełny obraz kategorii wartości

glvalue (i)

Wartość glvalue (uogólniona wartość lvalue) ma tożsamość. Użyjemy "i" jako skrótu dla "ma tożsamość".

lvalue (i&!m)

Wartość lvalue (rodzaj glvalue) ma tożsamość, ale nie jest przenośna. Są to zazwyczaj wartości odczytu-zapisu, które przekazywane są przez odwołanie, odwołanie stałe lub według wartości, jeśli kopiowanie jest tanie. Nie można powiązać l-wartości z referencją r-wartości.

xvalue (i&m)

Wartość xvalue (rodzaj glvalue, ale także rodzaj rvalue) ma tożsamość, a także jest wymienna. Może to być dawna wartość lvalue, którą zdecydowałeś się przenieść, ponieważ kopiowanie jest kosztowne, i będziesz ostrożny, aby nie uzyskać do niej dostępu później. Oto jak można przekształcić wartość lvalue w wartość xvalue.

struct A { ... };
A a; // a is an lvalue...
static_cast<A&&>(a); // ...but this expression is an xvalue.

W powyższym przykładzie kodu jeszcze nic nie przenieśliśmy. Utworzyliśmy jedynie xvalue, rzutując lvalue na nienazwaną referencję rvalue. Nadal można go zidentyfikować za pomocą jego nazwy lvalue; jednak jako xvalue jest teraz zdolny do bycia przenoszonym. Przyczyny przeniesienia i jak w rzeczywistości wygląda przenoszenie będą musiały poczekać na odrębny temat. Ale możesz pomyśleć o "x" w "xvalue" jako oznaczającym "tylko dla ekspertów", jeśli to pomoże. Rzutując wartość lvalue na wartość xvalue (rodzaj rvalue, pamiętaj), wartość staje się w stanie być powiązana z odwołaniem rvalue.

Poniżej przedstawiono dwa inne przykłady wartości xvalue — wywoływanie funkcji, która zwraca nienazwaną referencję rvalue i uzyskiwanie dostępu do elementu członkowskiego xvalue.

struct A { int m; };
A&& f();
f(); // This expression is an xvalue...
f().m; // ...and so is this.

prvalue (!i&m)

Wartość prvalue (czysta wartość rvalue; rodzaj wartości rvalue) nie ma tożsamości, ale jest przenośna. Są to zazwyczaj obiekty tymczasowe, wynik wywołania funkcji, która zwraca wartość, lub wynik ewaluacji dowolnego innego wyrażenia, które nie jest wartością glvalue.

rvalue (m)

Wartość rvalue jest wymienna. Użyjemy "m" jako skrótu dla "jest ruchomy".

Rvalue referencja zawsze odnosi się do rvalue (wartości, której zawartości zakładamy, że nie musimy zachowywać).

Czy referencja r-wartości jest sama w sobie r-wartością? Nienazwane odwołanie rvalue (takie jak pokazane w powyższych przykładach kodu xvalue) jest xvalue, więc tak, jest to rvalue. Preferuje powiązanie z parametrem funkcji referencyjnej rvalue, takim jak konstruktor przenoszenia. Odwrotnie (i być może w sposób nieintuicyjny), jeśli referencja rvalue ma nazwę, to wyrażenie składające się z tej nazwy jest wartością lvalue. Dlatego nie może być powiązany z parametrem referencji rvalue. Ale łatwo jest to zrobić — po prostu rzutuj go ponownie na nienazwaną referencję rvalue (xvalue).

void foo(A&) { ... }
void foo(A&&) { ... }
void bar(A&& a) // a is a named rvalue reference; so it's an lvalue.
{
    foo(a); // Calls foo(A&).
    foo(static_cast<A&&>(a)); // Calls foo(A&&).
}
A&& get_by_rvalue_ref() { ... } // This unnamed rvalue reference is an xvalue.

!i&!m

Rodzaj wartości, która nie ma tożsamości i nie jest wymienna, jest jedyną kombinacją, której jeszcze nie omówiliśmy. Możemy go jednak zignorować, ponieważ ta kategoria nie jest przydatnym pomysłem w języku C++.

Reguły zwijania odwołań

Wiele podobnych odwołań w wyrażeniu (odwołanie lvalue do odwołania lvalue lub odwołanie rvalue do odwołania rvalue) anuluje się nawzajem.

  • A& & zapada się do A&.
  • A&& && zapada się do A&&.

Wiele różnych odwołań w wyrażeniu sprowadza się do odwołania lvalue.

  • A& && zapada się do A&.
  • A&& & zapada się do A&.

Przekazywanie odwołań

W tej ostatniej sekcji kontrastują odwołania rvalue, które zostały już omówione, z inną koncepcją przekazywania odwołania. Zanim termin "referencja przekazywana dalej" został ukuty, niektórzy ludzie używali terminu "referencja uniwersalna".

void foo(A&& a) { ... }
  • A&& jest odwołaniem rvalue, jak widzieliśmy. Const i volatile nie mają zastosowania do odwołań rvalue.
  • foo akceptuje tylko rvalues typu A.
  • Powodem istnienia odwołań rvalue (takich jak A&&) jest to, aby można było napisać przeciążenie zoptymalizowane na przypadek, gdy przekazywany jest tymczasowy obiekt (lub inne rvalue).
template <typename _Ty> void bar(_Ty&& ty) { ... }
  • _Ty&& to przesyłania dalej odwołania. W zależności od tego, co przekażesz do bar, typ _Ty może być const/non-const niezależnie od volatile/non-volatile.
  • bar akceptuje wszelkie wartości lvalue lub rvalue typu _Ty.
  • Przekazywanie wartości lvalue powoduje, że odwołanie przekazujące stanie się _Ty& &&, co redukuje się do odwołania lvalue _Ty&.
  • Przekazywanie wartości rvalue powoduje, że przekazujące odwołanie staje się odwołaniem do rvalue _Ty&&.
  • Odwołania do przekazywania przyczyn (takie jak _Ty&&) istnieją nie do optymalizacji, ale do podjęcia tego, co przekazujesz do nich i do przekazywania go w sposób przejrzysty i wydajny. Prawdopodobnie napotkasz referencję przekazującą tylko wtedy, gdy piszesz (lub uważnie badasz) kod biblioteki — na przykład funkcję fabryczną, która przekazuje argumenty konstruktora.

Źródła

  • [Stroustrup, 2013] B. Stroustrup: Język programowania C++, czwarta edycja. Addison-Wesley. 2013.