A témával kapcsolatos írás első részében arról
volt szó, hogy az std::vector
egy push_back()
hívás esetén az új adatot
előbb másolja be a tömbbe, mint ahogyan a régi adatokat egy esetlegesen létrehozott új tömbbe áthelyezi.
Felokosítva a Noisy
osztályt, hogy rendelkezzen mozgató konstruktorral is (amelyik a „kiürülő”,
elmozgatott objektumok tartalmát -1-re állítja), ez jól látszik:
std::vector<Noisy> v(3);
Noisy n{5};
v.push_back(n);
Noisy copy ctor 5 új elem Noisy move ctor 0 Noisy move ctor 0 régiek áthelyezése Noisy move ctor 0 Noisy dtor -1 Noisy dtor -1 a régi tömb Noisy dtor -1
Ha a push_back()
függvény jobbértéket, azaz temporális objektumot kap, akkor ugyanez
a helyzet: előbb az új elem kerül az új tömbbe, és csak aztán a régiek:
v.push_back(Noisy{5});
Noisy{5} ctor temporálisé
Noisy move ctor 5
Noisy move ctor 0
Noisy move ctor 0
Noisy move ctor 0 a push_back()
belseje
Noisy dtor -1
Noisy dtor -1
Noisy dtor -1
Noisy dtor -1 temporálisé
A kérdés: vajon miért? Miért kell a temporálist is előbb mozgatni, amikor az független a tömbtől, és így igazából mindegy lenne a sorrend?
A válasz egyszerű: nem, nem mindegy a sorrend. Mégpedig azért nem, mert a paraméterként kapott jobbérték
objektum nem biztos, hogy temporális! Lehet olyan objektumról is szó, amelyet jobbértéknek tekintünk, amelyből
az elmozgatást kifejezetten megengedtük. Mégpedig egy std::move()
függvénnyel. És ha így van,
akkor a jobbértéknek tekintett objektum lehet akár a tömb része is:
std::vector<Noisy> v = { Noisy{1}, Noisy{2}, Noisy{3} };
v.push_back(std::move(v[1]));
Ez azért érdekes, mert ebben az esetben nem egy, hanem két mozgatás érinti ugyanazt az objektumot:
- Egyrészt a hívó kérte, hogy kerüljön egy új objektum a tömb végére, mégpedig a
v[1]
tartalmát átemelve. - Másrészt pedig a vektornak lehet szüksége emiatt átméretezésre, de az is érinti a
v[1]
-et.
Vagyis a hívó kérése ez:
A vektor sajátosságai miatt a technikai megvalósítás pedig ez lesz:
Fontos, hogy azt a hívó nem tudhatja, hogy a vektor mikor méreteződik át; ez a vektor döntése. A hívás végeredménye viszont ettől a döntéstől független kell legyen. Az teljesen biztos, hogy a 2-es számot tartalmazó objektum a tömb végére kell kerüljön.
Ezért a paraméterként kapott objektumot (amely egyébként a tömb v[1]
eleme is – ilyen a
referenciák természete) fogja előbb mozgatni, az kerül legelőször az új tömb végére. Csak utána mozgatja át a
többi elemet a tömbbe. Ilyenkor az eredeti tömb 1-es indexű eleme már üres (egyszer már el lett mozgatva),
vagyis az új helyre is egy üres objektum kerül. Végül pedig a 3 elemű tömb megszűnik.
A jobbérték referenciákkal kapcsolatos előadásban szó volt az objektumok élettartamáról: nevezetesen hogy a mozgató konstruktorok miatt nem csak a „létezik” és a „nem létezik” állapot létezik, hanem lehetséges üres, mozgató konstruktor által kiürített objektum létezése is. Az ilyenre a destruktort meg kell hívni, vagy új értéket adni neki.
Azt látjuk a fenti vektoros példa kapcsán, hogy előfordulhat, egy objektum tartalmát kétszer mozgatjuk:
egész pontosan, hogy egy objektumból kétszer próbáljuk meg kivenni annak tartalmát. Vajon ilyet szabad
csinálni? A fenti push_back(std::move(v[1]))
hívás csak akkor lehet helyes, ha igen. De
láthatóan erre a készítők gondoltak.
Érdemes ennek kapcsán megemlíteni néhány fogalmat, amelyik a C/C++ szabványokban is rendszeresen előkerül:
- Implementáció által definiált (implementation defined)
- Olyan dolgok, amelyeket nem köt meg a szabvány, pl. mert géptípustól függenek. Ilyen egy
int
mérete: architektúrától függ, ugyanakkor egyértelműen adott és dokumentált.- Nem specifikált (unspecified)
- Amikor azt mondjuk, hogy valami nem specifikált, akkor ugyan nem mondjuk meg, hogy pontosan minek kell történnie bizonyos helyzetben, de akármi történik, az nem vezethet a program működésképtelenségéhez. Ilyen például a függvényparaméterek kiértékelési sorrendje:
f(get_num(), get_num())
– itt nem tudhatjuk, melyik szám lesz az első, melyik a második paraméter. A fordító döntése.- Nem definiált (undefined)
- Ennél sincs megmondva, hogy minek kell történnie, de itt a program működésképtelenségéhez, kiszámíthatatlanságához juthatunk. Ilyen egy inicializálatlan változó értékének használata, vagy egy null pointer dereferálása.
Az elmozgatott objektum tartalma nem specifikált. Azaz lehet benne bármi (ahogy épp az adott objektumnál kényelmes vagy hatékony lehet a megvalósítás), nem mondjuk meg, hogy mi. Ezt a nem specifikált, üres tartalmat akár másodjára is el lehet mozgatni. Viszont azt szándékosan nem mondjuk rá, hogy a tartalma nem definiált: egy objektum kiürítése, vagy másodjára elmozgatása nem okozhatja a program elszállását.