Notitie
Voor toegang tot deze pagina is autorisatie vereist. U kunt proberen u aan te melden of de directory te wijzigen.
Voor toegang tot deze pagina is autorisatie vereist. U kunt proberen de mappen te wijzigen.
In dit onderwerp worden de verschillende categorieën waarden (en verwijzingen naar waarden) geïntroduceerd en beschreven die bestaan in C++:
- glvalue
- lvalue
- xlvalue
- prvalue
- rvalue
U hebt ongetwijfeld gehoord van lvalues en rvalues. Maar misschien denkt u niet aan ze in de bewoordingen die in dit onderwerp aan bod komen.
Elke expressie in C++ levert een waarde op die hoort bij een van de vijf hierboven genoemde categorieën. Er zijn aspecten van de C++-taal, de faciliteiten en regels, die een goed begrip van deze waardecategorieën en verwijzingen naar deze categorieën eisen. Deze aspecten omvatten het nemen van het adres van een waarde, het kopiëren van een waarde, het verplaatsen van een waarde en het doorsturen van een waarde naar een andere functie. Dit onderwerp gaat niet dieper in op al deze aspecten, maar biedt basisinformatie voor een solide kennis van deze aspecten.
De informatie in dit onderwerp is geformuleerd in termen van Stroustrups analyse van waardecategorieën aan de hand van de twee onafhankelijke eigenschappen identiteit en verplaatsbaarheid [Stroustrup, 2013].
Een lvalue heeft identiteit
Wat betekent het voor een waarde om identiteit te hebben? Als u het geheugenadres van een waarde hebt (of als u deze veilig kunt gebruiken), heeft de waarde een identiteit. Op die manier kunt u meer doen dan de inhoud van waarden vergelijken. U kunt deze vergelijken of onderscheiden op identiteit.
Een lvalue heeft identiteit. Het is nu een kwestie van alleen historisch belang dat de "l" in "lvalue" een afkorting is van "links" (zoals in, de linkerkant van een opdracht). In C++kan een lvalue aan de linkerkant of aan de rechterkant van een opdracht worden weergegeven. De 'l' in 'lvalue' helpt je dus eigenlijk niet om te begrijpen of te definiëren wat ze zijn. U hoeft alleen te begrijpen dat wat we een lvalue noemen, een waarde is die identiteit heeft.
Voorbeelden van expressies die lvalues zijn, zijn: een benoemde variabele of constante; of een functie die een verwijzing retourneert. Voorbeelden van expressies die geen lvalues zijn, zijn: een tijdelijk object; of een functie die op waarde retourneert.
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.
}
Hoewel het waar is dat lvalues een identiteit hebben, geldt dat ook voor xvalues. Verderop in dit onderwerp gaan we precies in op wat een xvalue is. Op dit moment moet u er rekening mee houden dat er een waardecategorie met de naam glvalue is (voor 'gegeneraliseerde lvalue'). De set glvalues is de superset van beide lvalues (ook wel bekend als klassieke lvalues) en xvalues. Dus, terwijl 'een lvalue heeft identiteit' waar is, is de volledige set dingen met identiteit de set glvalues, zoals wordt weergegeven in deze afbeelding.
Een rvalue is beweegbaar; een lvalue is niet
Maar er zijn waarden die geen glvalues zijn. Met andere woorden, er zijn waarden waarvoor u geen geheugenadres kunt verkrijgen (of u kunt er niet op vertrouwen dat het geldig is). We hebben enkele dergelijke waarden in het bovenstaande codevoorbeeld gezien.
Het niet hebben van een betrouwbaar geheugenadres klinkt als een nadeel. Maar in feite het voordeel van een dergelijke waarde is dat u deze kunt verplaatsen (wat meestal goedkoop is), in plaats van het te kopiëren (wat over het algemeen duur is). Als u een waarde verplaatst, betekent dit dat deze zich niet meer op de plaats bevindt waar deze vroeger was. Dus proberen er toegang toe te krijgen op de plek waar het voorheen stond, moet worden vermeden. Een bespreking van wanneer en hoe u een waarde verplaatst, valt buiten het bereik van dit onderwerp. Voor dit onderwerp moeten we alleen weten dat een verplaatsbare waarde bekend staat als een rvalue (of klassieke rvalue).
De "r" in "rvalue" is een afkorting van "right" (zoals in, de rechterkant van een opdracht). Maar u kunt rvalues en verwijzingen naar rvalues buiten toewijzingen gebruiken. De "r" in "rvalue" is dus niet waar je je op moet richten. U hoeft alleen te begrijpen dat wat we een rvalue noemen, een waarde is die beweegbaar is.
Een lvalue is daarentegen niet verplaatsbaar, zoals in deze afbeelding wordt weergegeven. Als een lvalue zou worden verplaatst, zou dat in strijd zijn met de definitie van lvalue. En dat zou een onverwacht probleem zijn voor code die redelijkerwijs mocht verwachten toegang te kunnen blijven houden tot de lvalue.
U kunt dus geen lvalue verplaatsen. Maar er is een soort glvalue (de verzameling dingen met identiteit) die u kunt verplaatsen—als u weet wat u doet (waarbij u er wel voor moet oppassen het na de verplaatsing niet meer te benaderen)—en dat is de xvalue. Verderop in dit onderwerp bekijken we dat idee wanneer we kijken naar het volledige beeld van waardecategorieën.
Rvalue-verwijzingen, en regels voor referentiebinding
In deze sectie wordt de syntaxis voor een verwijzing naar een rvalue geïntroduceerd. We zullen moeten wachten tot een ander onderwerp aan bod komt om verplaatsen en doorgeven uitvoerig te bespreken, maar het volstaat te zeggen dat rvalue-verwijzingen een noodzakelijk onderdeel van de oplossing voor deze problemen zijn. Voordat we echter naar rvalue-verwijzingen kijken, moeten we eerst duidelijker zijn over T&het ding dat we voorheen alleen 'een verwijzing' noemen. Het is echt 'een lvalue-verwijzing (niet-const)', die verwijst naar een waarde waarnaar de gebruiker van de verwijzing kan schrijven.
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.
Een lvalue-verwijzing kan worden gekoppeld aan een lvalue, maar niet aan een rvalue.
Vervolgens zijn er lvalue const-verwijzingen (T const&), die verwijzen naar objecten waarnaar de gebruiker van de verwijzing niet kan schrijven (bijvoorbeeld een constante).
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.
Een lvalue const-verwijzing kan worden gebonden aan een lvalue of aan een rvalue.
De syntaxis voor een verwijzing naar een rwaarde van het type T wordt geschreven als T&&. Een rvalue-verwijzing verwijst naar een verplaatsbare waarde: een waarde waarvan de inhoud niet hoeft te worden bewaard nadat we deze hebben gebruikt (bijvoorbeeld een tijdelijke waarde). Omdat het hele idee is om de waarde die aan een rvalue-referentie is gebonden te verplaatsen (en daarmee te wijzigen), zijn de kwalificaties const en volatile (ook bekend als cv-kwalificaties) niet van toepassing op rvalue-referenties.
template<typename T> T&& get_by_rvalue_ref() { ... } // Get by rvalue reference.
struct A { A(A&& other) { ... } }; // A move constructor takes an rvalue reference.
Een rvalue-verwijzing wordt gebonden aan een rvalue. Wat overloadresolutie betreft, wordt een rvalue bij voorkeur aan een rvalue-referentie gebonden in plaats van aan een const lvalue-referentie. Maar een rvalue-verwijzing kan niet worden gebonden aan een lvalue, omdat een rvalue-verwijzing verwijst naar een waarde waarvan wordt aangenomen dat we de inhoud niet hoeven te behouden (bijvoorbeeld de parameter voor een verplaatsingsconstructor).
U kunt ook een rvalue doorgeven waarbij een by-value-argument wordt verwacht, via kopieerconstructie (of via verplaatsingsconstructie als de rvalue een xwaarde is).
Een glvalue heeft identiteit; een prvalue niet
In dit stadium weten we wat identiteit heeft. En we weten wat er beweegbaar is en wat niet. Maar we hebben nog niet de set waarden genoemd die geen identiteit hebben. Deze set wordt de prvalue of pure rvalue genoemd.
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.
}
Het volledige beeld van waardecategorieën
Het blijft alleen om de informatie en illustraties hierboven te combineren tot één groot geheel.
glvalue (i)
Een glvalue (gegeneraliseerde lvalue) heeft een identiteit. We gebruiken 'i' als een afkorting voor 'heeft identiteit'.
lvalue (i&!m)
Een lvalue (een soort glvalue) heeft identiteit, maar is niet verplaatsbaar. Dit zijn meestal waarden die kunnen worden gelezen en geschreven en die als verwijzing, als const-verwijzing of, als kopiëren goedkoop is, als waarde worden doorgegeven. Een lvalue kan niet worden gebonden aan een rvalue-verwijzing.
xvalue (i&m)
Een xvalue (een soort glvalue, maar ook een soort rvalue) heeft identiteit en is ook beweegbaar. Dit kan een voorheen geldige lvalue zijn die u hebt besloten te verplaatsen omdat kopiëren duur is, en u zult er daarna voorzichtig mee zijn om deze niet meer te benaderen. U kunt als volgt een lvalue omzetten in een xvalue.
struct A { ... };
A a; // a is an lvalue...
static_cast<A&&>(a); // ...but this expression is an xvalue.
In het bovenstaande codevoorbeeld hebben we nog niets verplaatst. We hebben slechts een xvalue gecreëerd door een lvalue te casten naar een onbenoemde rvalue-referentie. Het kan nog steeds worden geïdentificeerd met de naam van de lvalue; maar als xwaarde is het nu in staat om te worden verplaatst. De redenen voor het verplaatsen en hoe bewegen er eigenlijk uitziet, moeten wachten op een ander onderwerp. Maar u kunt de 'x' in 'xvalue' ook opvatten als 'alleen voor experts' als dat helpt. Door een lvalue in een xvalue te casten (een soort rvalue, onthoud), kan de waarde vervolgens worden gebonden aan een rvalue-verwijzing.
Hier volgen twee andere voorbeelden van xvalues: het aanroepen van een functie die een verwijzing naar een niet-benoemde rvalue retourneert en toegang krijgt tot een lid van een xvalue.
struct A { int m; };
A&& f();
f(); // This expression is an xvalue...
f().m; // ...and so is this.
prvalue (!i&m)
Een prvalue (pure rvalue; een soort rvalue) heeft geen identiteit, maar is verplaatsbaar. Dit zijn meestal tijdelijke objecten, of het resultaat van het aanroepen van een functie die een waarde retourneert, of het resultaat van het evalueren van elke andere expressie die geen glvalue is.
rvalue (m)
Een rvalue is beweegbaar. We gebruiken 'm' als een afkorting voor 'is beweegbaar'.
Een rvalue-verwijzing verwijst altijd naar een rvalue (een waarde waarvan wordt uitgegaan dat we deze niet hoeven te behouden).
Maar is een rvalue-verwijzing zelf een rvalue? Een niet-benoemde rvalue-verwijzing (zoals in de bovenstaande voorbeelden van xvalue-code) is een x-waarde, dus ja, het is een rvalue. Het wordt bij voorkeur gebonden aan een functieparameter van het type rvalue-referentie, zoals die van een move-constructor. Omgekeerd (en misschien contra-intuïtief), als een rvalue-verwijzing een naam heeft, is de expressie die bestaat uit die naam een lvalue. Dit kan dus niet worden gebonden aan een verwijzingsparameter voor rvalue. Maar het is eenvoudig om ervoor te zorgen dat het dat toch doet — cast het gewoon opnieuw naar een naamloze rvalue-referentie (een 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
Het soort waarde dat geen identiteit heeft en niet beweegbaar is, is de ene combinatie die we nog niet hebben besproken. Maar we kunnen het negeren, omdat die categorie geen nuttig idee is in de C++-taal.
Regels voor samenvouwen van verwijzingen
Meerdere verwijzingen van hetzelfde type in een expressie (een lvalue-referentie naar een lvalue-referentie, of een rvalue-referentie naar een rvalue-referentie) heffen elkaar op.
-
A& &wordt samengevouwen inA&. -
A&& &&wordt samengevouwen inA&&.
Meerdere onwaarschijnlijke verwijzingen in een expressie worden samengevouwen tot een lvalue-verwijzing.
-
A& &&wordt samengevouwen inA&. -
A&& &wordt samengevouwen inA&.
Verwijzingen doorsturen
Deze laatste sectie contrasteert rvalue-verwijzingen, die we al hebben besproken, met het verschillende concept van een doorstuurverwijzing. Voordat de term "doorstuurverwijzing" werd bedacht, gebruikten sommigen de term "universele verwijzing".
void foo(A&& a) { ... }
-
A&&is een rvalue-verwijzing, zoals we al hebben gezien. const en volatile zijn niet van toepassing op rvalue-verwijzingen. -
fooaccepteert alleen rvalues van het type A. - De reden waarom rvalue-referenties (zoals
A&&) bestaan, is dat u een overbelasting kunt definiëren die is geoptimaliseerd voor situaties waarin een tijdelijke waarde (of een andere rvalue) wordt doorgegeven.
template <typename _Ty> void bar(_Ty&& ty) { ... }
-
_Ty&&is een forwarding-referentie. Afhankelijk van wat u aanbardoorgeeft, kan het type _Ty const of non-const zijn, onafhankelijk van volatile of niet-volatile. -
baraccepteert een lvalue of rvalue van het type _Ty. - Het doorgeven van een lvalue zorgt ervoor dat de doorstuurverwijzing wordt
_Ty& &&, wat samenvalt met de lvalue-verwijzing_Ty&. - Het doorgeven van een rvalue zorgt ervoor dat de doorstuurverwijzing de rvalue-verwijzing
_Ty&&wordt. - De reden voor het doorsturen van verwijzingen (zoals
_Ty&&) bestaat niet voor optimalisatie, maar om te nemen wat u aan hen doorgeeft en om deze transparant en efficiënt door te sturen. U zult waarschijnlijk alleen een forwarding reference tegenkomen als u bibliotheekcode schrijft (of grondig bestudeert), bijvoorbeeld een factoryfunctie die constructorargumenten doorgeeft.
Sources
- [Stroustrup, 2013] B. Stroustrup: The C++ Programming Language, vierde editie. Addison-Wesley. 2013.
Windows developer