Megosztás a következőn keresztül:


Értékkategóriák és ezekre mutató hivatkozások

Ez a témakör bemutatja és ismerteti a C++-ban létező különböző értékkategóriákat (és az értékekre mutató hivatkozásokat):

  • glvalue
  • lvalue
  • xlvalue
  • prvalue
  • rvalue

Bizonyára hallott már a lvalues és a rvalues. De lehet, hogy nem gondol rájuk a jelen témakör által ismertetett kifejezésekben.

A C++-ban minden kifejezés olyan értéket ad vissza, amely a fent felsorolt öt kategória egyikéhez tartozik. A C++ nyelvnek – annak lehetőségeinek és szabályainak – vannak olyan aspektusai, amelyek megkövetelik ezeknek az értékkategóriáknak a megfelelő megértését, valamint az ezekre mutató hivatkozásokat. Ilyen szempontok például az érték címének felvétele, egy érték másolása, egy érték áthelyezése és egy érték továbbítása egy másik függvénybe. Ez a témakör nem foglalkozik az összes ilyen szempont részletességével, de alapvető információkat nyújt azok szilárd megértéséhez.

A jelen témakör információi a Stroustrup értékkategóriáinak az identitás és a movability két független tulajdonsága alapján történő elemzését tekintik [Stroustrup, 2013].

Egy lvalue identitással rendelkezik

Mit jelent az, ha egy értéknek azidentitása? Ha rendelkezik (vagy felveheti) egy érték memóriacímét, és biztonságosan használhatja, akkor az érték identitással rendelkezik. Így többre is képes, mint az értékek tartalmának összehasonlítása – identitás alapján összehasonlíthatja vagy megkülönböztetheti őket.

Egy lvalue rendelkezik identitással. Most már csak történelmi érdekesség, hogy az "lvalue"-ban az "l" a "bal" rövidítése (mint a bal oldali egy kijelentésben). A C++-ban a bal oldali vagy a hozzárendelés jobb oldalán lvalue jelenhet meg. Az "lvalue"-ban az "l" valójában nem segít megérteni és meghatározni, hogy mik azok. Csak azt kell megértenie, hogy az lvalue-nak nevezett érték identitással rendelkezik.

A lvalues kifejezések például a következők: egy elnevezett változó vagy állandó; vagy olyan függvény, amely egy hivatkozást ad vissza. Példák olyan kifejezésekre, amelyek nem értékeket tartalmaznak: ideiglenes; vagy érték szerint visszaadott függvény.

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.
}

Bár igaz állítás, hogy a lvalues identitással rendelkezik, ez az xvaluesre is igaz. A jelen témakör későbbi részében pontosan megvizsgáljuk, hogy mit jelent a(z) xvalue. Egyelőre csak vegye figyelembe, hogy létezik egy glvalue nevű értékkategória (általánosított érték). A glvaluek készlete mindkét lvalue (más néven klasszikus lvalues) és az xvalues szuperhalmaza. Tehát, míg a "lvalue identitással rendelkezik" igaz, az identitással rendelkező dolgok teljes készlete a glvaluek készlete, ahogy az ábrán látható.

Egy lvalue identitással rendelkezik

Az rvalue mozgatható; az lvalue nem.

Vannak azonban olyan értékek, amelyek nem gl-valuek. Más szóval vannak olyan értékek, amelyeket nem tud memóriacímet szerezni (vagy nem támaszkodhat rá, hogy érvényes legyen). A fenti kód példában néhány ilyen értéket láttunk.

Az, hogy nem rendelkezik megbízható memóriacímmel, hátrányt jelent. De valójában az ilyen érték előnye, hogy áthelyezheti (ami általában olcsó), ahelyett, hogy lemásolja (ami általában drága). Az érték áthelyezése azt jelenti, hogy már nem azon a helyen van, ahol korábban volt. Ezért kerülni kell, hogy megpróbáljuk elérni a helyet, ahol korábban volt. A témakör hatókörén kívül esik egy érték áthelyezésének mikor és megvitatása. Ebben a témakörben csak azt kell tudnunk, hogy a mozgatható értékeket rvalue (vagy klasszikus rvalue) néven ismerjük.

Az "rvalue"-ban az "r" a "right" rövidítése (mint a feladat jobb oldalán). De a hozzárendeléseken kívül használhatja az rvaluákat és az rvaluákra mutató hivatkozásokat. Az "rvalue" "r" betűje nem az, amire összpontosítani kell. Csak azt kell megértenie, hogy amit rvalue-nak nevezünk, az egy mozgatható érték.

Ezzel szemben a lvalue nem mozgatható, ahogy az ábrán is látható. Ha egy lvalue lenne mozgatni, akkor ez ellentmondana az lvaluedefiníciójának. Ez egy váratlan probléma lenne azon kód számára, amely nagyon ésszerűen arra számított, hogy továbbra is hozzáférhet az lvalue-hez.

Egy értékérték mozgatható; az lvalue nem

Szóval nem lehet lvalue-t mozgatni. De van egyfajta glvalue (az identitással rendelkező dolgok készlete), amelyet áthelyezhet – ha tudja, hogy mit csinál (beleértve, hogy ügyeljen arra, hogy ne férhessen hozzá az áthelyezés után) – és ez az xvalue. A jelen témakör későbbi részében még egyszer áttekintjük ezt az ötletet, amikor áttekintjük az értékkategóriák teljes képét.

Rvalue-hivatkozások és referenciakötési szabályok

Ez a szakasz egy értékre mutató hivatkozás szintaxisát mutatja be. Meg kell várnunk, amíg egy másik témakör jelentős mértékben kezeli az áthelyezést és a továbbítást, de elegendő azt mondani, hogy a revalue-hivatkozások a problémák megoldásának szükséges részei. Mielőtt azonban áttekintenénk a revalue-hivatkozásokat, először tisztáznunk kell a T&– azt, amit korábban csak "referenciának" hívtunk. Ez valójában "lvalue (non-const) hivatkozás", amely egy olyan értékre utal, amelyre a hivatkozás felhasználója írhat.

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.

Egy lvalue-hivatkozás köthet egy lvalue-hoz, de nem rvalue-hoz.

Ezután vannak lvalue const-hivatkozások (T const&), amelyek olyan objektumokra hivatkoznak, amelyekre a hivatkozás felhasználója nem tud írni (például állandó).

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.

Az lvalue const-referencia egy lvalue-hez vagy egy rvalue-hoz kötődhet.

Az T típusú rvalue-ra való hivatkozás szintaxisa T&&. A revalue-hivatkozás egy ingó értékre utal – olyan értékre, amelynek tartalmát nem kell megőrizni a használat után (például ideiglenes). Mivel az rvalue-hivatkozáshoz kötött érték átvitele (és ezáltal módosítása) a cél, a const és volatile minősítők (más néven cv-minősítők) nem vonatkoznak az rvalue-hivatkozásokra.

template<typename T> T&& get_by_rvalue_ref() { ... } // Get by rvalue reference.
struct A { A(A&& other) { ... } }; // A move constructor takes an rvalue reference.

A rvalue-referencia egy rvalue-hoz kötődik. Valójában a túlterhelés feloldása szempontjából az rvalue esetén előnyben részesül az, ha rvalue-hivatkozáshoz kapcsolódik, mint ha egy lvalue const-hivatkozáshoz. Az rvalue-referenciák azonban nem köthetők lvalue-hez, mert mint már említettük, az rvalue-hivatkozás egy olyan értékre utal, amelynek tartalmát feltételezzük, hogy nem kell megőriznünk (például egy áthelyezési konstruktor paraméterét).

Átadhat egy rvalue-t olyan helyzetben, ahol értékszerinti argumentumra van szükség, másolási konstrukcióval (vagy áthelyezési konstrukcióval, ha az rvalue xvalue).

A glvalue identitással rendelkezik; a prvalue nem

Ebben a szakaszban tudjuk, mi az identitás. És tudjuk, mi mozgatható és mi nem. De még nem neveztük el azokat az értékeket, amelyek nem identitással. A készletet prvalue-ként, vagy tiszta rvalue-ként ismerik.

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.
}

A glvalue identitással rendelkezik; a prvalue nem

Az értékkategóriák teljes képe

A fenti információkat és ábrákat csak egyetlen, nagy képpé kell egyesíteni.

Az értékkategóriák teljes képe

glvalue (i)

A glvalue (általánosított lvalue) identitással rendelkezik. Az "i" rövidítést használjuk arra, hogy "rendelkezik identitással".

lvalue (i és !m)

Az lvalue (egyfajta glvalue) identitással rendelkezik, de nem mozgatható. Ezek általában olyan írás-olvasási értékek, amelyeket hivatkozással vagy const-hivatkozással ad át, vagy érték szerint, ha a másolás olcsó. Az lvalue nem köthető rvalue-hivatkozáshoz.

xvalue (i&m)

Az xvalue (egyfajta glvalue, de egyfajta rvalue) identitással rendelkezik, és mozgatható is. Ez lehet egy korábbi balérték, amit úgy döntött, hogy áthelyez, mert a másolás költséges, és ügyel majd arra, hogy utána ne férjen hozzá. Az alábbiakban bemutatjuk, hogyan alakíthat egy lvalue-t xvalue-vá.

struct A { ... };
A a; // a is an lvalue...
static_cast<A&&>(a); // ...but this expression is an xvalue.

A fenti kód példájában még nem helyeztünk át semmit. Csupán egy xvalue-t hoztunk létre úgy, hogy egy lvalue-t egy meg nem nevezett rvalue-hivatkozásra adtunk. Továbbra is azonosítható a lvalue neve alapján; de xvalue-ként most már képes mozgatni. Az áthelyezés okait és azt, hogy a tényleges áthelyezés hogyan néz ki, egy másik témakörben kell tárgyalnunk. De gondolhatod úgy, hogy az "xvalue" kifejezésben az "x" jelentése "csak szakértők számára van", ha ez segít. Ha egy lvalue-t xvalue-vá alakítunk (ami egy rvalue egy fajtája, ne feledje), az érték ezáltal képessé válik arra, hogy rvalue-referenciához legyen kötve.

Íme két másik példa az xvalues-re: meghív egy függvényt, amely névtelen r-érték hivatkozást ad vissza, és elér egy xvalue tulajdonságot.

struct A { int m; };
A&& f();
f(); // This expression is an xvalue...
f().m; // ...and so is this.

prvalue (!i&m)

A prvalue (tiszta rvalue; egyfajta rvalue) nem rendelkezik identitással, de mozgatható. Ezek általában ideiglenesek, vagy egy érték alapján visszaadott függvény meghívásának eredménye, vagy bármely más kifejezés kiértékelésének eredménye, amely nem glvalue.

rvalue (m)

Az rvalue áthelyezhető. Az "m" rövidítést fogjuk használni a "mozgatható" kifejezéshez.

A rvalue referenciák mindig egy rvalue-re hivatkoznak (olyan értékre, amelynek feltételezzük, hogy a tartalmát nem szükséges megőriznünk).

De egy rvalue-hivatkozás maga is rvalue? Egy meg nem nevezett rvalue-hivatkozás (mint a fenti xvalue példakódban bemutatottak) egy xvalue, tehát igen, ez egy rvalue. Inkább egy rvalue referenciafüggvény paraméteréhez, például egy áthelyezési konstruktorhoz van kötve. Ezzel szemben (és talán ellen-intuitív módon), ha egy rvalue hivatkozásnak van neve, akkor az abból a névből álló kifejezés egy lvalue. Ezért nem rvalue referenciaparaméterhez kötni. Ezt azonban könnyű megtenni – csak adja vissza egy névtelen rvalue-hivatkozáshoz (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

Az az érték, amely nem rendelkezik identitással, és nem mozgatható, az egyetlen kombináció, amiről még nem beszéltünk. De figyelmen kívül hagyhatjuk, mert ez a kategória nem hasznos ötlet a C++ nyelven.

Referencia-összecsukási szabályok

Egy kifejezésben több azonos típusú hivatkozás (lvalue hivatkozás lvalue hivatkozásra vagy rvalue hivatkozás rvalue hivatkozásra) kioltják egymást.

  • A A& &A&-é alakul.
  • A A&& &&A&&-é alakul.

Egy kifejezésben lévő több különböző hivatkozás lvalue-hivatkozássá alakulhat át.

  • A A& &&A&-é alakul.
  • A A&& &A&-é alakul.

Továbbítási hivatkozások

Ez az utolsó szakasz a korábban már tárgyalt rvalue-hivatkozásokat állítja szembe a továbbítási referenciaeltérő fogalmával. A "továbbítási hivatkozás" kifejezés megalkotása előtt egyes emberek az "univerzális hivatkozás" kifejezést használták.

void foo(A&& a) { ... }
  • A&& egy rvalue referencia, amint azt láttuk. A const és a volatile nem vonatkozik a rvalue-hivatkozásokra.
  • foo csak Atípusú értékeket fogad el.
  • Az rvalue-hivatkozások (például A&&) azért léteznek, hogy olyan túlterhelést generáljon, amely egy ideiglenes (vagy más érték) átadása esetén van optimalizálva.
template <typename _Ty> void bar(_Ty&& ty) { ... }
  • A _Ty&& egy továbbítási referencia. Attól függően, hogy mit ad át bar, a _Ty típusa lehet const/nem-const, függetlenül az illékony vagy nem illékony állapottól.
  • bar elfogadja a _Tytípusú lvalue-t vagy rvalue-t.
  • Az lvalue átadása miatt a továbbítási hivatkozás _Ty& &&lesz, ami lvalue-referenciává alakul _Ty&.
  • Az rvalue átadása miatt a továbbítási hivatkozás az rvalue-referencia _Ty&&lesz.
  • A továbbítási hivatkozások (például _Ty&&) azért léteznek, mert nem optimalizálásra, hanem az, hogy átvegye azokat, és transzparensen és hatékonyan továbbítsa őket. Valószínűleg csak akkor talál továbbítási hivatkozást, ha kódtárkódot ír (vagy tanulmányoz). Ilyen például egy gyári függvény, amely konstruktorargumentumokon továbbít.

Források

  • [Stroustrup, 2013] B. Stroustrup: The C++ Programming Language, Fourth Edition. Addison-Wesley. 2013.