Poznámka:
Přístup k této stránce vyžaduje autorizaci. Můžete se zkusit přihlásit nebo změnit adresáře.
Přístup k této stránce vyžaduje autorizaci. Můžete zkusit změnit adresáře.
Toto téma představuje a popisuje různé kategorie hodnot (a odkazy na hodnoty), které existují v jazyce C++:
- glvalue
- lvalue
- xlvalue
- prvalue
- rvalue
Určitě jste slyšeli o lvalues a rvalues. Ale možná si je nepřemýšlíte v pojmech, které toto téma představuje.
Každý výraz v jazyce C++ poskytuje hodnotu, která patří do jedné z pěti kategorií uvedených výše. Existují aspekty jazyka C++ – jeho zařízení a pravidla – které vyžadují správné porozumění těmto kategoriím hodnot a také odkazy na ně. Mezi tyto aspekty patří přebírání adresy hodnoty, kopírování hodnoty, přesunutí hodnoty a předávání hodnoty na jinou funkci. Toto téma se podrobně nezabíjejí do všech těchto aspektů, ale poskytuje základní informace pro jejich solidní pochopení.
Informace v tomto tématu jsou formulovány na základě Stroustrupovy analýzy kategorií hodnot pomocí dvou nezávislých vlastností: identity a přemístitelnosti [Stroustrup, 2013].
Hodnota má identitu.
Co znamená, že hodnota má identitu? Pokud máte (nebo můžete vzít) adresu paměti hodnoty a bezpečně ji použít, pak má tato hodnota identitu. Tímto způsobem můžete provádět více než porovnání obsahu hodnot – můžete je porovnat nebo odlišit podle identity.
Lvalue má identitu. Dnes je už jen historickou zajímavostí, že „l“ v „lvalue“ je zkratkou slova „left“ (tj. levá strana přiřazení). V jazyce C++ se může hodnota lvalue zobrazit vlevo nebo vpravo od přiřazení. "L" v "lvalue", pak, ve skutečnosti nepomůže pochopit ani definovat, co jsou. Stačí pochopit, že to, co nazýváme lvalue, je hodnota, která má identitu.
Příklady výrazů, které jsou lvalues, zahrnují: pojmenovanou proměnnou nebo konstantu; nebo funkci, která vrací odkaz. Mezi příklady výrazů, které nejsou levými hodnotami, patří: dočasná hodnota nebo funkce, která vrací hodnotu.
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.
}
I když je to pravdivý výrok, že hodnoty lvalue mají identitu, to platí i pro hodnoty xvalue. Podíváme se přesně na to, co je hodnota xvalue později v tomto tématu. Prozatím mějte na paměti, že existuje kategorie hodnot s názvem glvalue (pro "generalized lvalue"). Množina glvalues je nadmnožina obou hodnot (označovaných také jako klasické hodnoty lvalue) a hodnot x. Takže i když je pravda, že „lvalue má identitu“, úplnou množinou věcí, které mají identitu, je množina glvalue, jak je znázorněno na tomto schématu.
Hodnota rvalue je pohyblivá; hodnota lvalue není
Existují ale hodnoty, které nejsou glvalues. Jinými slovy, existují hodnoty, pro které nemůžete získat adresu paměti (nebo se na ni nemůžete spolehnout). Některé takové hodnoty jsme viděli v příkladu kódu výše.
Nemít spolehlivou paměťovou adresu zní jako nevýhoda. Ale ve skutečnosti je výhodou takové hodnoty to, že ji můžete přesunout (což je obecně levné), místo abyste ji kopírovali (což je obecně drahé). Přesunutí hodnoty znamená, že už není na místě, kde byla. Je tedy třeba se vyhnout pokusům o přístup k tomu na místě, kde to dříve bývalo. Diskuze o tom, kdy a jak přesunout hodnotu, je mimo rozsah tohoto tématu. V tomto tématu potřebujeme jen vědět, že se pohyblivá hodnota označuje jako rvalue (nebo klasická hodnota rvalue).
„r“ v termínu „rvalue“ je zkratkou slova „right“ (tedy pravá strana přiřazení). Můžete ale používat r-hodnoty a odkazy na r-hodnoty i mimo přiřazení. „r“ v „rvalue“ tedy není to, na co je třeba se zaměřit. Stačí pochopit, že to, co nazýváme rvalue, je hodnota, která je pohyblivá.
Lvalue, naopak, není pohyblivý, jak je znázorněno na tomto obrázku. Pokud by se hodnota lvalue přesunula, pak by to bylo v rozporu s velmi definicí lvalue. A byl by to neočekávaný problém pro kód, který zcela oprávněně očekával, že bude moci nadále přistupovat k lvalue.
Lvalue tedy nelze přesunout. Ale existuje určitý druh glvalue (množina věcí s identitou), který můžete přesunout — pokud víte, co děláte (včetně toho, že k němu po přesunu nesmíte přistupovat) —, a tím je xvalue. Později v této kapitole se k této myšlence ještě jednou vrátíme, až se podíváme na ucelený obraz kategorií hodnot.
Odkazy na Rvalue a pravidla vazby odkazů
Tato část představuje syntaxi odkazu na rvalue. Na podrobnější výklad o přesouvání a předávání si budeme muset počkat do některého z dalších témat, ale prozatím stačí říci, že odkazy na rvalue jsou nezbytnou součástí řešení těchto problémů. Než se ale podíváme na rvalue reference, musíme si nejprve lépe ujasnit, co je T&— to, čemu jsme dříve říkali prostě „reference“. Ve skutečnosti jde o „nekonstantní odkaz na levostrannou hodnotu“ („an lvalue reference“), tj. odkaz na hodnotu, do které může uživatel odkazu zapisovat.
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.
Odkaz na lvalue se může vázat na lvalue, ale ne na rvalue.
Pak existují odkazy na hodnoty const (T const&), které odkazují na objekty, na které uživatel odkazu nemůže zapisovat (například konstanta).
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.
Konstantní odkaz na lvalue lze navázat na lvalue nebo na rvalue.
Syntaxe odkazu na hodnotu rvalue typu T je zapsána jako T&&. Odkaz rvalue odkazuje na pohyblivou hodnotu – hodnotu, jejíž obsah po jeho použití nepotřebujeme zachovat (například dočasný). Vzhledem k tomu, že celý smysl spočívá v přesunutí hodnoty vázané na odkaz rvalue const (a tím v její úpravě), volatile kvalifikátory (označované také jako kvalifikátory cv) se na odkazy rvalue nevztahují.
template<typename T> T&& get_by_rvalue_ref() { ... } // Get by rvalue reference.
struct A { A(A&& other) { ... } }; // A move constructor takes an rvalue reference.
Odkaz rvalue se sváže s hodnotou rvalue. Ve skutečnosti, pokud jde o rozlišení přetížení, hodnota rvalue dává přednost být vázána na odkaz rvalue než na odkaz const hodnoty lvalue. Rvalue reference se ale nemůže navázat na lvalue, protože, jak už jsme řekli, rvalue reference odkazuje na hodnotu, u níž se předpokládá, že její obsah nemusíme zachovat (řekněme parametr pro přesouvací konstruktor).
Můžete také předat rvalue tam, kde se očekává argument předávaný hodnotou, prostřednictvím kopírovací konstrukce (nebo prostřednictvím přesunové konstrukce, pokud je rvalue xvalue).
Hodnota glvalue má identitu; hodnota prvalue ne
V této fázi víme, co má identitu. A víme, co je pohyblivé a co není. Zatím jsme ale sadu hodnot, které nemají identitu, ještě nepojmenovali. Tato množina je známá jako prvalue nebo 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.
}
Úplný obrázek kategorií hodnot
Zkombinuje jenom informace a ilustrace uvedené výše do jediného velkého obrázku.
glvalue (i)
Hodnota glvalue (generalizovaná lvalue) má identitu. Jako zkratku pro „má identitu“ použijeme „i“.
lvalue (i&!m)
Hodnota lvalue (druh glvalue) má identitu, ale není pohyblivá. Obvykle se jedná o hodnoty určené ke čtení i zápisu, které předáváte odkazem nebo konstantním odkazem, případně hodnotou, pokud je kopírování levné. Lvalue nemůže být svázána s rvalue referencí.
xvalue (i&m)
Hodnota xvalue (druh glvalue, ale také druh rvalue) má identitu a je také pohyblivý. Může to být dřívější lvalue, kterou jste se rozhodli přesunout, protože kopírování je náročné, a dáte si pozor, abyste k ní později už nepřistupovali. Tady je postup, jak převést lvalue na hodnotu xvalue.
struct A { ... };
A a; // a is an lvalue...
static_cast<A&&>(a); // ...but this expression is an xvalue.
V příkladu kódu výše jsme zatím nic nepřesunuli. Pouze jsme vytvořili hodnotu xvalue přetypováním lvalue na nepojmenovaný odkaz rvalue. Stále jej lze identifikovat jeho názvem lvalue; jako xvalue jej však nyní lze přesunout. Důvody, proč to přesunout, a to, jak takové přesunutí ve skutečnosti vypadá, budou muset počkat na jiné téma. Pokud to ale pomůže, můžete si "x" v "xvalue" představit jako "expert-only". Přetypováním lvalue na xvalue (druh rvalue, nezapomeňte) pak lze tuto hodnotu navázat na rvalue referenci.
Tady jsou dva další příklady hodnot xvalue – volání funkce, která vrací nepojmenovaný odkaz rvalue a přístup k členu xvalue.
struct A { int m; };
A&& f();
f(); // This expression is an xvalue...
f().m; // ...and so is this.
prvalue (!i&m)
Hodnota prvalue (pure rvalue; druh rvalue) nemá identitu, ale je pohyblivá. Obvykle se jedná o dočasné volání funkce, která vrací hodnotu, nebo výsledek vyhodnocení jakéhokoli jiného výrazu, který není glvalue.
rvalue (m)
Hodnota rvalue je pohyblivá. "m" použijeme jako zkratku pro "je pohyblivý".
Odkaz rvalue vždy odkazuje na hodnotu rvalue (hodnota, jejíž obsah se předpokládá, že nepotřebujeme zachovat).
Ale je odkaz na rvalue sám o sobě rvalue? Nepojmenovaný odkaz rvalue (podobně jako v příkladech kódu xvalue výše) je hodnota xvalue, takže ano, je to rvalue. Preferuje navázání na parametr funkce typu rvalue reference, například na parametr přesouvacího konstruktoru. Naopak (a možná proti intuici), pokud má rvalue reference jméno, pak je výraz tvořený tímto jménem lvalue. Proto nemůže být vázán na parametr odkazu rvalue. Je ale snadné toho docílit — stačí jej znovu přetypovat na nepojmenovaný odkaz na r-hodnotu (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
Druh hodnoty, která nemá identitu a není pohyblivý, je jedna kombinace, kterou jsme zatím neprobírali. Můžeme ji ale ignorovat, protože tato kategorie není užitečnou myšlenkou v jazyce C++.
Pravidla slučování odkazů
Více odkazů stejného typu ve výrazu (odkaz lvalue na odkaz lvalue nebo odkaz rvalue na odkaz rvalue) se navzájem ruší.
-
A& &se sbalí naA&. -
A&& &&se sbalí naA&&.
Více různých referencí ve výrazu se redukuje na l-hodnotovou referenci.
-
A& &&se sbalí naA&. -
A&& &se sbalí naA&.
Přeposílací odkazy
Tato závěrečná část staví do kontrastu rvalue reference, o nichž jsme již mluvili, s odlišným konceptem forwarding reference. Než byl zaveden termín „předávací reference“, někteří lidé používali termín „univerzální reference“.
void foo(A&& a) { ... }
-
A&&je rvalue reference, jak jsme již viděli. Const a volatile se nevztahují na odkazy rvalue. -
foopřijímá pouze hodnoty rvalue typu A. - Důvod, proč existují reference na rvalue (například
A&&), je ten, abyste mohli vytvořit přetížení optimalizované pro případ, kdy je předána dočasná hodnota (nebo jiné rvalue).
template <typename _Ty> void bar(_Ty&& ty) { ... }
-
_Ty&&je přeposílající reference. V závislosti na tom, co předátebar, může být typ _Ty const nebo non-const bez ohledu na to, zda je volatile, nebo non-volatile. -
barpřijímá všechny hodnoty lvalue nebo rvalue typu _Ty. - Předání l-hodnoty způsobí, že se forwarding reference stane
_Ty& &&, která se zredukuje na l-hodnotovou referenci_Ty&. - Předání rvalue způsobí, že se předávací reference stane referencí rvalue
_Ty&&. - Důvod, proč existují předávací reference (například
_Ty&&), není optimalizace, ale to, aby převzaly to, co jim předáte, a transparentně a efektivně to předaly dál. S předávací referencí se pravděpodobně setkáte pouze tehdy, pokud píšete (nebo podrobně studujete) kód knihovny — například tovární funkci, která přeposílá argumenty konstruktoru.
Sources
- [Stroustrup, 2013] B. Stroustrup: Programovací jazyk C++, čtvrtá edice. Addison-Wesley. 2013.
Windows developer