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
- l-waarde
- xlvalue
- prvalue
- r-waarde
U hebt ongetwijfeld gehoord van lvalues en rvalues. Maar misschien denkt u niet aan hen zoals ze in dit onderwerp worden gepresenteerd.
Elke expressie in C++ levert een waarde op die deel uitmaakt van 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 weergegeven in termen van Stroustrups analyse van waardecategorieën aan de hand van de twee onafhankelijke eigenschappen identiteit en beweegbaarheid [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 worden weergegeven aan de linkerkant of rechts van een opdracht. De 'l' in 'lvalue' helpt u niet om te begrijpen of te definiëren wat ze werkelijk 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 niet lvalues zijn: een tijdelijke expressie; of een functie die een 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 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 is het voordeel van een dergelijke waarde dat u het kunt verplaatsen (wat meestal goedkoop is), in plaats van het te kopiëren (wat meestal duur is). Als u een waarde verplaatst, betekent dit dat deze zich niet meer op de plaats bevindt waar deze vroeger was. Dus proberen het te benaderen waar het vroeger was, is iets om te voorkomen. Een bespreking van wanneer en hoe een waarde te verplaatsen is niet relevant voor 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 dan niet het ding 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. Het zou een onverwacht probleem zijn voor code die redelijkerwijs had verwacht om toegang tot de lvalue te behouden.
U kunt dus geen lvalue verplaatsen. Maar er is een soort glvalue (de verzameling van objecten met een identiteit) die je kunt verplaatsen, als je weet wat je doet (waarbij je ervoor zorgt dat je er na de verplaatsing geen toegang meer toe hebt), 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 verwijzingsbinding
In deze sectie wordt de syntaxis voor een verwijzing naar een rvalue geïntroduceerd. We moeten wachten totdat een ander onderwerp ingaat op een uitgebreide bespreking van verplaatsen en doorsturen, maar het is voldoende om te zeggen dat rvalue-verwijzingen een noodzakelijk onderdeel van de oplossing van die 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 zich binden 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). Aangezien het hele punt is om de waarde die aan een rvalue-referentie is gebonden te verplaatsen (waardoor deze wordt gewijzigd), zijn de const- en volatile-kwalificaties (ook wel 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 bindt aan een rvalue. In feite heeft een rvalue-bij de overbelastingsresolutie de voorkeur dat aan een rvalueverwijzing gebonden wordt in plaats van aan een lvalue const-verwijzing. 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 geen naam voor de set waarden die geen identiteit hebben. Deze set is bekend als de prvalue, of als de 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.
}
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 lees-/schrijfwaarden die u doorgeeft via verwijzing, const-verwijzing, of op waarde als het kopiëren goedkoop is. 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 vroegere lvalue zijn die u hebt besloten te verplaatsen, omdat kopiëren duur is, en u zult zorgvuldig ervoor zorgen dat u het daarna niet meer benadert. 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 gemaakt door een lvalue te casten naar een verwijzing naar een niet-benoemde rvalue. Het kan nog steeds worden geïdentificeerd met de naam van de lvalue; Maar als xwaarde is het nu in staat 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' beschouwen als 'alleen voor ervaren gebruikers' 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. Deze zijn meestal tijdelijke objecten, of het resultaat van het aanroepen van een functie die een waarde teruggeeft, 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 aangenomen dat we de inhoud niet hoeven te behouden).
Maar is een rvalue-verwijzing zelf een rvalue? Een niet-benoemde rvalue-verwijzing (zoals in de xvalue-codevoorbeelden hierboven) is een xwaarde, dus inderdaad, het is een rvalue. Het geeft de voorkeur aan gekoppeld te worden aan een rvalue-verwijzingsfunctieparameter, 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. Het kan dus niet worden gebonden aan een verwijzingsparameter voor rvalue. Maar het is eenvoudig om dit te bereiken—u hoeft het alleen weer te casten naar een niet-benoemde rvalue-verwijzing (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 gelijke verwijzingen in een expressie (een lvalue-verwijzing naar een lvalue-verwijzing of een rvalue-verwijzing naar een rvalue-verwijzing) 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
In deze laatste sectie worden rvalue-verwijzingen, die we al hebben besproken, met het verschillende concept van een verwijzing voor doorsturen. Voordat de term "doorstuurverwijzing" werd geïntroduceerd, gebruikten sommige mensen de term "universele verwijzing".
void foo(A&& a) { ... }
-
A&&is een rvalue-verwijzing, zoals we hebben gezien. Const en volatile zijn niet van toepassing op rvalue-verwijzingen. -
fooaccepteert alleen rvalues van het type A. - De reden waarom rvalue-verwijzingen (zoals
A&&) bestaan, is zodat u een overload-functie kunt maken die geoptimaliseerd is voor het geval van een tijdelijke waarde (of andere rvalue) die wordt doorgegeven.
template <typename _Ty> void bar(_Ty&& ty) { ... }
-
_Ty&&is een doorgangsverwijzing. Afhankelijk van wat u aanbardoorgeeft, kan het type _Ty const/niet-const zijn, onafhankelijk van of het vluchtig/niet-vluchtig is. -
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&. - Door een rvalue door te geven, wordt de doorstuurverwijzing een rvalue-verwijzing
_Ty&&. - 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 doorstuurverwijzing tegenkomen als u bibliotheekcode schrijft (of nauw onderzoekt), bijvoorbeeld een fabrieksfunctie die doorstuurt op constructorargumenten.
Bronnen
- [Stroustrup, 2013] B. Stroustrup: De programmeertaal C++, Vierde Editie. Addison-Wesley. 2013.