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ývá všemi těmito aspekty, ale poskytuje základní informace pro jejich dobré pochopení.
Informace v tomto tématu jsou z pohledu Stroustrupovy analýzy kategorií hodnot podle dvou nezávislých vlastností: identity a pohyblivosti [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. Už jde jen o historickou zajímavost, že "l" v "lvalue" je zkratka pro "left" (ve smyslu levá strana přiřazení). V jazyce C++ se může hodnota lvalue zobrazit na levé nebo napravo od přiřazení. Písmeno "L" ve slově "lvalue" vám ve skutečnosti nepomůže pochopit ani definovat, co to znamená. 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. Příklady výrazů, které jsou nejsou hodnoty lvalue, zahrnují: dočasný; nebo funkci, 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 na to, co přesně znamená 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žinou jak lvalue (také známé jako klasické lvalue), tak xvalue. I když je pravda, že „lvalue má identitu“, kompletní soubor věcí, které mají identitu, je soubor glvalue, jak je znázorněno na tomto obrázku.
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 na kterou se nemůžete spolehnout, že bude platná). Některé takové hodnoty jsme viděli v příkladu kódu výše.
Nedostatek spolehlivé adresy paměti vypadá jako nevýhoda. Ale ve skutečnosti výhodou hodnoty, jako je tato, je, že ji můžete přesunout (což je obecně levné), namísto jejího kopírování (což je obecně drahé). Přesunutí hodnoty znamená, že už není na místě, kde byla. Snažit se získat přístup k místu, kde bývalo, je něčím, čemu je třeba se vyhnout. 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 pohyblivá hodnota je známá jako rvalue (nebo klasické rvalue).
"r" v "rvalue" je zkratka "right" (jako v pravé straně přiřazení). Můžete ale použít rvalues a odkazy na hodnoty r, mimo přiřazení. Na "r" ve slově "rvalue" by se nemělo zaměřovat. 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 definicí lvalue. To by byl neočekávaný problém s kódem, který velmi rozumně očekával, že může pokračovat v přístupu k *lvalue*.
Takže nemůžete přesunout hodnotu lvalue. Ale je druh glvalue (množina entit s identitou), které můžete přesunout – pokud víte, co děláte (přičemž dáte pozor, abyste na ni po přesunu nepřistupovali) – a to je xvalue. Až později v tomto tématu ještě jednou podrobně probereme témata kategorií hodnot, vrátíme se k této myšlence.
Odkazy na Rvalue a pravidla vazby odkazů
Tato část představuje syntaxi odkazu na rvalue. Budeme muset počkat na další téma, abychom mohli přejít k podrobnému probírání přesunu a předávání, ale postačí říci, že rvalue odkazy jsou nezbytnou součástí řešení těchto problémů. Než se ale podíváme na odkazy na rvalue, musíme být nejdřív jasnější o T&– o věci, o které jsme dříve mluvili jako o "odkazu". Je to opravdu "lvalue (non-const) odkaz", který odkazuje na hodnotu, na kterou může uživatel odkazu napsat.
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 hodnotu rvalue.
Pak existují odkazyT 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.
Odkaz const lvalue se může svázat s lvalue nebo s rvalue.
Syntaxe odkazu na 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 hlavním účelem je přesunout (a tím modifikovat) hodnotu vázanou na referenci rvalue, kvalifikátory const a volatile (také označované jako cv-kvalifikátory) se na reference 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 váže na hodnotu rvalue. Ve skutečnosti, pokud jde o rozlišení přetížení, rvalue má přednost, aby se vázal na odkaz typu rvalue raději než na odkaz typu const pro lvalue. Ale odkaz typu rvalue nemůže být svázán s lvalue, protože, jako jsme řekli, se odkaz typu rvalue vztahuje na hodnotu, kterou nepotřebujeme zachovávat (například parametr konstruování pomocí přesunutí).
Můžete také předat rvalue, kde je očekáváno argument by-value, prostřednictvím kopírovací konstrukce (nebo přesunutí konstrukce, pokud rvalue je 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 nenazvali sadu hodnot, které nemají identitu. Tato sada se označuje jako prvalue, nebo čisté 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 pro čtení i zápis, které předáváte odkazem nebo const odkazem, nebo hodnotou, pokud je kopírování levné. L-hodnotu nelze přiřadit k odkazu na R-hodnotu.
xvalue (i&m)
Hodnota xvalue (druh glvalue, ale také druh rvalue) má identitu a je také pohyblivý. Může to být erstwhile lvalue, kterou jste se rozhodli přesunout, protože kopírování je nákladné, a budete opatrní, abyste k němu později 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 typu xvalue tím, že jsme přetypovali lvalue na nepojmenovaný odkaz na rvalue. Lze jej stále identifikovat názvem lvalue; ale jako hodnota xvalue je nyní schopné přesunu. Důvody pro přesunutí a to, jak přesunutí skutečně vypadá, budou muset počkat na další diskuzi. 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) se hodnota stává schopnou být vázána 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 (čistá rvalue; typ 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 na rvalue vždy odkazuje na rvalue (hodnotu, jejíž obsah nepředpokládáme, že musíme zachovat).
Ale je rvalue reference samo o sobě rvalue? nepojmenovaný odkaz rvalue (podobně jako v příkladech kódu xvalue výše) je xvalue, takže to je rvalue. Dává přednost tomu, aby byl vázán na parametr referenční funkce rvalue, jako je například konstruktor přesunutí. Naopak (a možná protichůdně intuitivně), pokud má odkaz rvalue název, pak je výraz, který z tohoto názvu vychází, lvalue. Proto nemůže být vázána na parametr odkazu rvalue. Je to ale snadné udělat – stačí ho znovu přetypovat na nepojmenovaný odkaz na 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
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 zhroucení odkazů
Více referencí stejného typu ve výrazu (lvalue reference na lvalue reference, nebo rvalue reference na rvalue reference) se navzájem ruší.
-
A& &se zhroutí doA&. -
A&& &&se zhroutí doA&&.
Více různých odkazů ve výrazu se zhroutí na odkaz typu lvalue.
-
A& &&se zhroutí doA&. -
A&& &se zhroutí doA&.
Přeposílací odkazy
Tato poslední část porovnává odkazy rvalue, které jsme již probírali, s odlišným konceptem odkazů pro přeposílání. Předtím, než byl zaveden termín "odkaz na předávání", někteří lidé používali termín "univerzální odkaz".
void foo(A&& a) { ... }
-
A&&je odkaz na rvalue, jak jsme viděli. Const a volatile se nevztahují na odkazy rvalue. -
foopřijímá pouze hodnoty rvalue typu A. - Důvod, proč existují odkazy na rvalue (například
A&&), je ten, že můžete vytvořit přetížení funkcí, které je optimalizováno pro případ, kdy je předávána dočasná (nebo jiná rvalue) hodnota.
template <typename _Ty> void bar(_Ty&& ty) { ... }
-
_Ty&&je referenční předávání. V závislosti na tom, co předáte dobar, může být typ _Ty konstantní/nekonstantní nezávisle na volatilní/nevolatilní. -
barpřijímá všechny hodnoty lvalue nebo rvalue typu _Ty. - Předání lvalue způsobí, že univerzální reference se stane
_Ty& &&, který kolabuje na lvalue referenci_Ty&. - Předání r-hodnoty způsobí, že předávací odkaz se stane odkazem na r-hodnotu
_Ty&&. - Důvod, proč existuje předávání odkazů (například
_Ty&&), není pro optimalizaci, ale aby přijaly to, co jim předáte, a poté to transparentně a efektivně předaly dál. Pravděpodobně se setkáte s odkazy na přeposílání pouze v případě, že píšete (nebo podrobně zkoumáte) kód knihovny – například tovární funkci, která předává argumenty konstruktoru.
Zdroje
- [Stroustrup, 2013] B. Stroustrup: Programovací jazyk C++, čtvrtá edice. Addison-Wesley. 2013.