Not
Å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 presenterad utifrån Stroustrups analys av värdekategorierna, baserat på de två oberoende egenskaperna identitet och flyttbarhet [Stroustrup, 2013].
En lvalue har en identitet
Vad innebär det att ett värde har identitet? Om du har (eller kan ta) minnesadressen för ett värde och använda det 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 förekomma till vänster eller till höger om en tilldelning. "Då hjälper 'L' i 'lvalue' dig faktiskt inte att förstå eller definiera vad det är." 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 lvalues är: ett temporärt objekt; eller en funktion som returnerar ett 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 heter glvalue (för "generaliserad lvalue"). Uppsättningen glvalues är totalmängden för både lvalues (kallas även klassiska lvalues) och xvalues. Så även om "en lvalue har identitet" är sant, är den fullständiga uppsättningen saker som har identitet uppsättningen glvalues, som visas i den här bilden.
En rvalue kan flyttas; en lvalue kan inte.
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 för att flytta ett värde ligger utanför området 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 klassisk rvalue).
"r" i "rvalue" är en förkortning av "right" (som i höger sida av en tilldelning). Men du kan använda rvärden och referenser till rvärden 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, omvänt, är inte rörlig, som visas i den här bilden. 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 ett slags 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 behandlas mer ingående angående rörelse och vidarebefordran, men det räcker med att säga att rvärde-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 om T&– det vi tidigare kallade bara "en referens". Det handlar om "en lvalue-referens, en icke-konstant referens, som refererar 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 användaren av referensen inte kan skriva (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 lvalue const-referens kan binda till en lvalue eller till ett 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 (vilket ändrar) värdet som är bundet till en rvalue-referens, gäller const och volatile kvalificerare (även kallade cv-qualifiers) 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 binder till ett rvalue. Faktum är att, när det gäller överladdningslösning, föredrar en rvalue-att binds till en rvalue-referens snarare än till en lvalue const-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 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 använder "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 läs- och skrivvärden som du överför med referens eller konstant referens, eller efter värde om kopiering är billigt. 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 en före detta lvalue som du har valt att flytta eftersom kopiering är dyrt, och du är noga med att inte komma åt 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 är det nu kapabel att 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 "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 variabler, 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 är en glvalue.
rvalue (m)
En rvalue kan flyttas. Vi använder "m" som en förkortning för "är flyttbar".
En rvalue-referens refererar alltid till ett rvalue (ett värde vars innehåll förutsätts att vi inte behöver bevara).
Men, är en rvalue-referens i sig själv 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 föredrar att bindas till en rvalue-referensfunktionsparameter, som till exempel 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. Det kan därför inte bindas till en referensparameter för rvalue. Men det är enkelt att göra detta – bara konvertera 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 sammanfaller till en lvalue-referens.
-
A& &&komprimeras tillA&. -
A&& &komprimeras tillA&.
Vidarekopplingsreferenser
Det här sista avsnittet kontrasterar rvalue-referenser, som vi redan har diskuterat, med det olika begreppet referens för vidarebefordran. 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 typen A. - Orsaken till att rvalue-referenser (till exempel
A&&) finns är att du kan skapa en överlagring som är optimerad för att en temporär (eller annan rvalue) skickas.
template <typename _Ty> void bar(_Ty&& ty) { ... }
-
_Ty&&är en referens för vidarebefordran. Beroende på vad som anges tillbar, typ _Ty kan vara konstant/icke-konstant oberoende av flyktig/icke-flyktig. -
baraccepterar alla lvalue eller rvalue av typen _Ty. - Att skicka en lvalue gör att vidarebefordringsreferensen blir
_Ty& &&, vilket komprimeras till lvalue-referensen_Ty&. - När du överför ett rvalue, blir vidarebefordrande referensen en rvalue-referens
_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å ett transparent och effektivt sätt. 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.
Källor
- [Stroustrup, 2013] B. Stroustrup: C++-programmeringsspråket, fjärde utgåvan. Addison-Wesley. 2013.