Kommentar
Åtkomst till den här sidan kräver auktorisering. Du kan prova att logga in eller ändra kataloger.
Åtkomst till den här sidan kräver auktorisering. Du kan prova att ändra kataloger.
Det här avsnittet introducerar och beskriver de olika kategorier av värden (och referenser till värden) som finns i C++:
- glvalue
- Lvalue
- xlvalue
- prvalue
- rvalue
Du har säkert hört talas om lvalues och rvalues. Men du kanske inte tänker på dem i de termer som det här ämnet presenterar.
Varje uttryck i C++ ger ett värde som tillhör någon av de fem kategorier som anges ovan. Det finns aspekter av C++-språket – dess faciliteter och regler – som kräver en korrekt förståelse av dessa värdekategorier samt referenser till dem. Dessa aspekter omfattar att ta adressen till ett värde, kopiera ett värde, flytta ett värde och vidarebefordra ett värde till en annan funktion. Det här avsnittet går inte in på alla dessa aspekter på djupet, men det ger grundläggande information för en gedigen förståelse för dem.
Informationen i det här avsnittet är formulerad utifrån Stroustrups analys av värdekategorier enligt de två oberoende egenskaperna identitet och flyttbarhet [Stroustrup, 2013].
En lvalue har en identitet
Vad innebär det att ett värde har en identitet? Om du har (eller kan hämta) minnesadressen för ett värde och använder den på ett säkert sätt, har värdet identitet. På så sätt kan du göra mer än att jämföra innehållet i värden – du kan jämföra eller särskilja dem efter identitet.
En lvalue har identitet. Det är nu en fråga av endast historiskt intresse att "l" i "lvalue" är en förkortning av "vänster" (som i, den vänstra sidan av en tilldelning). I C++ kan en lvalue visas till vänster eller till höger om en tilldelning. "L" i "lvalue" hjälper dig alltså egentligen inte att vare sig begripa eller definiera vad det handlar om. Du behöver bara förstå att det vi kallar en lvalue är ett värde som har en identitet.
Exempel på uttryck som är lvalues är: en namngiven variabel eller konstant; eller en funktion som returnerar en referens. Exempel på uttryck som inte är lvalues är: en tillfällig; eller en funktion som returnerar efter värde.
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.
}
Även om det är ett sant påstående att lvalues har identitet, gäller det även för xvalues. Vi går in på exakt vad en xvalue är senare i det här avsnittet. För tillfället är det bara att tänka på att det finns en värdekategori som kallas glvalue (för "generaliserad lvalue"). Mängden glvalues är övermängden till både lvalues (även kända som klassiska lvalues) och xvalues. Så även om "en lvalue har identitet" är sant, är hela mängden av sådant som har identitet mängden glvalues, som illustrationen visar.
Ett rvalue är flyttbart; ett lvalue är inte flyttbart
Men det finns värden som inte är glvalues. Det finns med andra ord värden som du inte kan hämta en minnesadress för (eller så kan du inte lita på att den är giltig). Vi såg några sådana värden i kodexemplet ovan.
Att inte ha en tillförlitlig minnesadress låter som en nackdel. Men i själva verket är fördelen med ett värde som det att du kan flytta det (vilket i allmänhet är billigt), snarare än att kopiera det (vilket i allmänhet är dyrt). Att flytta ett värde innebär att det inte längre finns på den plats där det brukade vara. Så att försöka komma åt den på den plats där den brukade vara är något att undvika. En diskussion om när och hur du flyttar ett värde ligger utanför omfånget för det här ämnet. I det här avsnittet behöver vi bara veta att ett flyttbart värde kallas för ett rvalue (eller klassiskt rvalue).
"r" i "rvalue" är en förkortning av "right" (som i högersidan i en tilldelning). Men du kan använda rvalues och referenser till rvalues utanför tilldelningar. "r" i "rvalue" är alltså inte det man ska fokusera på. Du behöver bara förstå att det vi kallar en rvalue är ett värde som kan flyttas.
En lvalue kan däremot inte flyttas, vilket framgår av den här illustrationen. Om en lvalue skulle flytta, skulle det motsäga själva definitionen av lvalue. Och det skulle vara ett oväntat problem för kod som mycket rimligtvis förväntas kunna fortsätta att komma åt lvalue.
Så du kan inte flytta en lvalue. Men det finns en typ av glvalue (uppsättningen saker med identitet) som du kan flytta – om du vet vad du gör (inklusive att vara noga med att inte komma åt den efter flytten) – och det är xvalue. Vi återkommer till den idén en gång till senare i det här avsnittet när vi tittar på den fullständiga bilden av värdekategorier.
Rvalue-referenser och referensbindningsregler
Det här avsnittet introducerar syntaxen för en referens till ett rvalue. Vi måste vänta på att ett annat ämne ska gå in i en betydande behandling av att flytta och vidarebefordra, men det räcker med att säga att rvalue-referenser är en nödvändig del av lösningen på dessa problem. Innan vi tittar på rvalue-referenser måste vi dock först vara tydligare – T&det vi tidigare kallade bara "en referens". Det är egentligen "en lvalue-referens (icke-konstant)", det vill säga en referens till ett värde som den som använder referensen kan skriva till.
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.
En lvalue-referens kan binda till en lvalue, men inte till ett rvalue.
Sedan finns det lvalue const-referenser (T const&), som refererar till objekt som referensanvändaren inte kan skriva till (till exempel en konstant).
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.
En konstant lvalue-referens kan bindas till en lvalue eller en rvalue.
Syntaxen för en referens till ett rvalue av typen T skrivs som T&&. En rvalue-referens refererar till ett flyttbart värde – ett värde vars innehåll vi inte behöver bevara när vi har använt det (till exempel ett tillfälligt). Eftersom hela poängen är att flytta från (och därmed ändra) det värde som är bundet till en rvalue-referens gäller const- och volatile-kvalificerare (även kallade cv-kvalificerare) inte för rvalue-referenser.
template<typename T> T&& get_by_rvalue_ref() { ... } // Get by rvalue reference.
struct A { A(A&& other) { ... } }; // A move constructor takes an rvalue reference.
En rvalue-referens kan bindas till ett rvalue. Faktum är att när det gäller överlagringsupplösning föredrar ett rvalue att bindas till en rvalue-referens snarare än till en konstant lvalue-referens. Men en rvalue-referens kan inte binda till en lvalue eftersom, som vi har sagt, en rvalue-referens refererar till ett värde vars innehåll det antas att vi inte behöver bevara (till exempel parametern för en flyttkonstruktor).
Du kan också skicka ett rvalue där ett eftervärdesargument förväntas, via kopieringskonstruktion (eller via flyttkonstruktion om rvalue är en xvalue).
En glvalue har identitet; en prvalue inte
I det här skedet vet vi vad som har identitet. Och vi vet vad som är flyttbart och vad som inte är det. Men vi har ännu inte namngett den uppsättning värden som inte har någon identitet. Den uppsättningen kallas prvalue eller ren 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.
}
Den fullständiga bilden av värdekategorier
Det återstår bara att kombinera informationen och illustrationerna ovan till en enda helhet.
glvalue (i)
En glvalue (generaliserad lvalue) har identitet. Vi kommer att använda "i" som en förkortning för "har identitet".
lvalue (i&!m)
En lvalue (ett slags glvalue) har identitet, men kan inte flyttas. Det här är vanligtvis värden som kan läsas och skrivas och som du skickar vidare med referens eller som const-referens, eller med värde om kopiering är billig. En lvalue kan inte bindas till en rvalue-referens.
xvalue (i&m)
En xvalue (en typ av glvalue, men också en typ av rvalue) har identitet och kan också flyttas. Detta kan vara ett före detta lvalue som du har bestämt dig för att flytta eftersom det är dyrt att kopiera, och du kommer att vara noga med att inte använda det efteråt. Så här kan du omvandla en lvalue till ett xvalue.
struct A { ... };
A a; // a is an lvalue...
static_cast<A&&>(a); // ...but this expression is an xvalue.
I kodexemplet ovan har vi inte flyttat något ännu. Vi har bara skapat ett xvalue genom att casta en lvalue till en namnlös rvalue-referens. Det kan fortfarande identifieras med dess lvalue-namn; men som ett xvalue kan det nu flyttas . Orsakerna till att flytta den, och hur flytten faktiskt ser ut, måste vänta på ett annat ämne. Men du kan tänka på "x" i "xvalue" som att det betyder "endast för experter", om det hjälper. Genom att omvandla en lvalue till en xvalue (ett slags rvalue, kom ihåg) kan värdet sedan bindas till en rvalue-referens.
Här är två andra exempel på xvalues – anropa en funktion som returnerar en namnlös rvalue-referens och få åtkomst till en medlem i en xvalue.
struct A { int m; };
A&& f();
f(); // This expression is an xvalue...
f().m; // ...and so is this.
prvalue (!i&m)
En prvalue (ren rvalue, ett slags rvalue) har inte identitet, men kan flyttas. Dessa är vanligtvis temporära objekt, eller resultatet av att anropa en funktion som returnerar ett värde, eller resultatet av att utvärdera vilket annat uttryck som helst som inte utgör ett glvalue.
rvalue (m)
En rvalue kan flyttas. Vi använder "m" som en kortform för "är flyttbar".
En rvalue-referens refererar alltid till ett rvalue (ett värde vars innehåll det antas att vi inte behöver bevara).
Men är en rvalue-referens i sig en rvalue? En namnlös rvalue-referens (som de som visas i xvalue-kodexemplen ovan) är en xvalue, så ja, det är ett rvalue. Den binds helst till en funktionsparameter som är en rvalue-referens, som den i en flyttkonstruktor. Omvänt (och kanske kontraintuitivt), om en rvalue-referens har ett namn, är uttrycket som består av det namnet ett lvalue. Därför kan den inte bindas till en rvalue-referensparameter. Men det är lätt att få det att göra det – bara kasta det till en namnlös rvalue-referens (en xvalue) igen.
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
Den typ av värde som inte har identitet och som inte kan flyttas är den enda kombination som vi ännu inte har diskuterat. Men vi kan bortse från det, eftersom den kategorin inte är en användbar idé på C++-språket.
Regler för referenskollaps
Flera liknande referenser i ett uttryck (en lvalue-referens till en lvalue-referens eller en rvalue-referens till en rvalue-referens) avbryter varandra.
-
A& &komprimeras tillA&. -
A&& &&komprimeras tillA&&.
Flera olika referenser i ett uttryck kollapsar till en lvalue-referens.
-
A& &&komprimeras tillA&. -
A&& &komprimeras tillA&.
Vidarebefordrande referenser
I det här sista avsnittet jämförs rvalue-referenser, som vi redan har diskuterat, med det annorlunda begreppet vidarebefordrande referens. Innan termen "vidarebefordringsreferens" myntades använde vissa personer termen "universell referens".
void foo(A&& a) { ... }
-
A&&är en rvalue-referens, som vi har sett. Const och volatile gäller inte för rvalue-referenser. -
fooaccepterar endast rvalu av typ A. - Anledningen till att rvalue-referenser (som
A&&) finns är att du kan skriva en överlagrad funktion som är optimerad för fallet där ett temporärt objekt (eller ett annat rvalue) skickas.
template <typename _Ty> void bar(_Ty&& ty) { ... }
-
_Ty&&är en referens för vidarebefordran. Beroende på vad du skickar tillbarkan typen _Ty vara const eller icke-const, oberoende av volatile eller icke-volatile. -
baraccepterar alla lvalue eller rvalue av typen _Ty. - Att skicka med ett lvalue gör att vidarebefordringsreferensen blir
_Ty& &&, vilket kollapsar till lvalue-referensen_Ty&. - Om du skickar in ett rvalue omvandlas vidarebefordringsreferensen till rvalue-referensen
_Ty&&. - Anledningen till att vidarebefordringsreferenser (till exempel
_Ty&&) finns är inte för optimering, utan för att ta det du skickar till dem och vidarebefordra det på transparent och effektivt. Det är troligt att du bara stöter på en referens för vidarebefordran om du skriver (eller studerar) bibliotekskod, till exempel en fabriksfunktion som vidarebefordrar konstruktorargument.
Sources
- [Stroustrup, 2013] B. Stroustrup: C++-programmeringsspråket, fjärde utgåvan. Addison-Wesley. 2013.
Windows developer