Az elavult std::auto_ptr
Czirkos Zoltán · 2022.06.21.
Mi volt a baj a régi std::auto_ptr-rel? Miért került ki a szabványból? Hogy volt megvalósítva?
Miért nem használjuk már az std::auto_ptr
-t? Miért döntöttek úgy, hogy teljesen
kitörlik azt a szabványból? Mi volt az, ami ekkora problémát jelentett?
Tulajdonos szemantika
Az előadáson láttuk, hogy az auto_ptr
tulajdonos szemantikát valósít meg, méghozzá
olyan módon, hogy egy dinamikusan foglalt objektumért pontosan egy auto_ptr
felel.
A pointer objektum törlődésekor törölni kell a kezelt objektumot is:
template <typename T>
class auto_ptr {
public:
explicit auto_ptr(T *ptr = nullptr): ptr_(ptr) {}
~auto_ptr() { delete ptr_; }
/* ... */
private:
T *ptr_;
Ezért egy ilyen objektum „lemásolásakor”, történjen az akár egy konstruktorral, akár az értékadó operátorral, az eredeti pointertől el kell venni a kezelt objektumot. Így biztosítva azt, hogy egy kezelt objektum csak egy pointerhez tartozzon:
template <typename T>
class auto_ptr {
public:
T * release() {
T *temp = ptr_;
ptr_ = nullptr; /* elfelejti */
return temp; /* innentől a hívó felel érte */
}
auto_ptr(auto_ptr &the_other) // nem konstans ref
: auto_ptr(the_other.release())
{
}
auto_ptr & operator=(auto_ptr &the_other) { // itt sem konstans
if (ptr_ != the_other.ptr_) {
delete ptr_;
ptr_ = the_other.release();
}
}
};
Problémák: másolás = másolás?
A gond az, hogy ezektől a függvényektől másolást szoktunk meg, ennek ellenére
a nekik paraméterként adott objektumokat módosítják. Ez szokatlan működés, amely
a legváratlanabb helyzetekben okozhat fennakadást. Pl. auto_ptr
típusú
függvényparaméter esetén:
void foo(auto_ptr<int> p);
void bar() {
auto_ptr<int> p1(new int);
*p1 = 17;
foo(p1); // törlődik az int
std::cout << *p1; // nullptr, segfault
Gond volt akkor is, ha auto_ptr
-eket tettünk tárolókba. Sok algoritmus feltételezi, hogy a
„másolások” nem teszik tönkre az eredeti objektumokat. Például a gyorsrendezés úgy működik, hogy egy
kiválasztott elemet kiemel egy segédváltozóba, hogy aztán a többi elemet ahhoz hasonlítsa. A segédváltozó pedig
végül megszűnik, tehát rendezés közben eltűnne a tömb egyik eleme.
A temporális objektumok és a nem konstans referenciák
Van még egy problémánk, amely a nyelvi elemekkel kapcsolatos. A temporális objektumok jobbértékek, amelyekre
más paraméterátadási szabályok vonatkoznak, mint a balértékekre. A C++-ban a jobbértékeket, azaz a temporális
objektumokat nem tudják átvenni paraméterként az olyan függvények, amelyek referenciát várnak
(T&
); csak azok, amelyek konstans referenciát (T const &
) vagy értéket
(T
).
Emlékezzünk vissza a C-s terminológiára: balérték az, ami egy értékadás bal
oldalán állhat, vagy más megközelítésben, aminek a memóriacíme képezhető. Jobbérték minden
más. Az int i
deklaráció után az i
változó hivatkozása
egy balérték; i = 0
és &i
is helyes. Az i +
1
és az 5
kifejezések pedig jobbértékek; az i + 1 =
0
és &5
kifejezések helytelenek. C++-ban jobbértékek a
temporális objektumok, és természetesen minden olyan függvényhívás értéke is,
amely objektumot ad, nem pedig referenciát.
Ha nem konstans referencia átvehetne jobbértéket, elég furcsa dolgokat lehetne írni. Pl.:
void novel(int &i) {
++i;
}
novel(3);
A „másoló” konstruktorunk paramétere viszont most semmiképpen nem lehet
konstans referencia, hanem csak sima referencia. Ezért a másoló konstruktor most
nem tud temporális objektumot átvenni. Ami azért baj, mert az alábbiak teljesen
ésszerű használatát mutatják az osztálynak, sőt pont így képzeljük el ez egészet.
Az első esetben a visszatérési értéket próbáljuk egy temporális kifejezéssel
inicializálni, a második esetben pedig a függvény visszatérési értéke a
temporális objektum, amely alapján a p
-t inicializálni szeretnénk.
Egyikre sem illik rá a másoló konstruktor nem konstans referencia paramétere:
auto_ptr<int> create_int() {
return auto_ptr<int>{new int{}}; // temporális!
}
auto_ptr<int> p = create_int(); // temporális!
A megoldási lehetőséget az adja, hogy bár temporális objektumot nem adhatunk
nem konstans referencia paraméterű függvénynek – azt mégis megengedi a nyelv,
hogy egy temporális objektum tagfüggvényét meghívjuk, még akkor is, ha
az a tagfüggvény nem konstans minősítésű. Kell ezért egy segédobjektum
(auto_ptr_helper
), amely a másolásban segédkezni fog, méghozzá a következőképpen.
- Egy
auto_ptr_helper
objektum ugyanolyan pointert tud tárolni, mint azauto_ptr
párja, de a tároláson kívül semmit nem csinál. - Az
auto_ptr
-nek van egyauto_ptr
paraméterű konstruktora:auto_ptr(auto_ptr&)
(ez a „másoló” konstruktor), és egyauto_ptr_helper
paraméterű konstruktora:auto_ptr(auto_ptr_helper)
. - A
return auto_ptr<int>{new int{}};
utasításban egy objektumot inicializálunk, a visszatérési értéket. Ebből látja a fordító, hogy egyauto_ptr
konstruktort kell hívni. Megnézi, milyenek vannak. Két konstruktor jöhet szóba. - Az első az
auto_ptr
paraméterű másoló konstruktor. Ezt nem engedik a szabályok, mert a temporális egy jobbérték, amihez nem köthető a nem konstans referencia. - A másik konstruktor egy
auto_ptr_helper
-t vár. A temporális nem ilyen típusú, de még lehet, hogy konvertálható. Megnézi ezért, van-eauto_ptr
→auto_ptr_helper
konverzió. - Ezt a konverziót kell megírnunk. Ennek a konverziós függvénynek a dolga,
hogy a kezelt objektumot elengedje (
release()
), és az ideiglenesen létrehozottauto_ptr_helper
objektumra bízza. Az az objektum létrejön, a temporálisauto_ptr
pedig kiürül, null pointerré válik, így a destruktora nem fog csinálni semmit. - A kezelt objektum pointerét az
auto_ptr_helper
-től a másikauto_ptr
átveszi, és eltárolja. Az ideiglenesauto_ptr_helper
-re ezután már nincs szükség.
Mindez C++ nyelven:
template <typename T>
class auto_ptr {
public:
/* a segédosztály */
class auto_ptr_helper {
public:
T *ptr_;
auto_ptr_helper(T *ptr_) : ptr{ptr} {}
};
/* auto_ptr -> auto_ptr_helper */
operator auto_ptr_helper () {
return auto_ptr_helper{this->release()};
}
/* auto_ptr_helper -> auto_ptr */
auto_ptr(auto_ptr_helper helper): ptr_{helper.ptr_} {
}
};
auto_ptr<int> create_int() {
return auto_ptr<int>{new int{}}; // return static_cast<auto_ptr_helper>(...);
}
Mi látszik ebből? Leginkább az, hogy a nyelvünk nem tud kifejezni valamit, amire a tulajdonos szemantikájú okos pointer leírásához szükségünk lenne. A C++98-ban még nem tudtuk megkülönböztetni egymástól nyelvi szinten a jobbértékeket és a balértékeket. C++11 óta szerencsére erre van már lehetőség.