Noexcept move constructor
Mondtuk, hogy jó lenne, ha minden move konstruktor noexcept minősítést kaphatna, mert akkor tudják a többi függvények, hogy egy (ismeretlen) objektum erőforrása mozgatható. Meg azt is látjuk, hogy legtöbbször a move konstruktor szinte triviális, egy-két pointerművelet vagy ilyesmi. De akkor hogy is van ez? Meg lehet minden move konstruktort noexceptesre csinálni, és csak szintaktikai elvárás, hogy kiírjuk a noexceptet, vagy nem lehet mindet, és okkal kell figyelni ennek kiírására? Miért?
(Ha nem lehet, mutass ellenpéldát. Ha lehet, akkor magyarázd meg, miért. Rosszul vagy hiányosan implementált osztály nem lehet pro, sem kontra érv.)
Megoldás
Több példa is van erre:
- Konstans adattag vagy referencia adattag. (Az utóbbi olyasmi, mint egy konstans pointer.) Ezek nem módosíthatóak, ezért nem is mozgatható el a tartalmuk.
- Ha más, egyéb okból nem mozgatható részei vannak az objektumnak. Az
std::list
pl. strázsás láncolt listát épít, mégpedig azért, hogy a végéről (std::list::end
), vagyis az utolsó utáni elemtől vissza lehessen jönni--
operátorral az utolsóra. Egyben azt is ígéri, hogy bármilyen műveletet végzünk a listával, a nem módosított elemek iterátora érvényes marad. Ha van egyend
iterátorunk egy listára, és annak a listának a tartalmát elmozgatjuk, azend
iterátor érvényes kell maradjon; ami azt jelenti, hogy a strázsát nem mozgathatjuk, az meg kell maradjon az eredeti listánál. Emiatt az új lista, amibe áthelyezzük az objektumokat, a strázsát nem veheti el, hanem saját strázsákat kell létrehoznia. - Operációs rendszer vagy hardver limitációk, pl. egy
std::mutex
. - Olyan esetek, amikor harmadik szereplővel végzett művelet is bekerül a képbe. Ilyen a SuperNoisy osztály is; ebben a közös tárolóba regisztrált objektumok miatt a mozgató konstruktornak többlet feladata van, amelyik nem csak a két objektumot érinti (amiből és amibe mozgatni kell az adatot), hanem a tárolót is, amelyhez hozzá kell adni az új objektumot.
Noexcept függvények
A főprogramban két kódrészlet van. Külön kell őket vizsgálnod. Melyiknél mi történik? Miért?
#include <iostream>
struct X {
X() = default;
X(X const &) { throw 0; }
};
void f_val(X param) noexcept {
}
void f_ref(X const & param) noexcept {
X copy(param);
}
int main() {
X x1;
try {
std::cout << "f_val hivasa" << std::endl;
f_val(x1);
} catch (...) {}
try {
std::cout << "f_ref hivasa" << std::endl;
f_ref(x1);
} catch (...) {}
}
Megoldás
Az f_val()
függvényben tényleg nem keletkezhet kivétel. Ugyan a hívásához létrejönne az
x1
objektum másolata az X
osztály másoló konstruktorához, amely kivételt dob,
de ez a kivétel nem a függvényben keletkezik, hanem még azon kívül.
Az f_ref()
esetében más a helyzet. Ennek hívása kedvéért csak egy referencia jön létre,
a függvénytörzsbe belépés előtt kivétel még nem keletkezik. A törzsben viszont igen, a másolat létrehozásakor.
Mivel a függvény noexcept
minősítővel rendelkezik, ez a kivétel nem jut ki a függvényből,
hanem megszakad a program futása.
Noexcept mozgató konstruktor
Mi a hiba az alábbi programrészben? Kettőt is tudsz mutatni!
class String {
/* ... */
};
String::String(String && moved) noexcept {
data_ = moved.data_;
len_ = moved.len_;
moved.data_ = new char[1];
moved.data_[0] = '\0';
moved.len_ = 0;
}
Megoldás
Az első hiba a kód jelenlegi formájában a noexcept
kulcsszó. A függvény törzsében lévő kód igenis dobhat kivételt;
ha egy noexcept
függvényben ilyen történik, a program futása megszakad. Ennél jobb ötlet lenne a noexcept
kulcsszót törölni; ha kivétel dobódik, legalább kaphassa el azt a hívó.
A másik hiba az, hogy az új dinamikus tömb valószínűleg felesleges. A moved
objektum tartalmát ez a konstruktor
elmozgatja; az objektumnak érvényes állapotban kell maradnia, de lehet üres is (destructable / assignable state). Az üres objektum
tartalmát senkinek nem szabad felhasználnia, senki nem építhet arra a kódban, hogy mi tárolódik benne. Jobb, hatékonyabb lenne,
ha az a tömb nem jönne létre, hanem a moved.data_
értékét nullptr
-re állítanánk.
Ha így teszünk, akkor egyébként a függvény visszakaphatja a noexcept
minősítőjét:
String::String(String && moved) noexcept {
data_ = moved.data_;
len_ = moved.len_;
moved.data_ = nullptr;
moved.len_ = 0;
}
std::stack top és pop
Az std::stack
osztálynak két függvénye van, amelyik a stack tetején lévő elemmel dolgozik. A top()
visszaadja ezt az elemet (T&
értékű függvény), de nem veszi ki a stackből; a pop()
kitörli az elemet,
de nem adja vissza (void értékű függvény). Miért van ez így? Miért nincs olyan, amelyik kitörli és visszaadja a visszatérési
értékében? Miért nincs olyan, amely egy referencia által adott helyre másolja? Miért nincs olyan, hogy... [egyéb verzió]? Mutasd be
kódrészleten a másféle megoldások problémáit!
A kivételkezelés tesztelése
A kivételek általában kivételes helyzetekben keletkeznek – ahogy a nevük is mutatja. A legtöbb kivétel olyan körülmény miatt jön létre, aminek nem kellene bekövetkeznie; pl. név nélküli fájlba próbálunk elmenteni valamilyen tartalmat, vagy elfogy a memória.
A kivételeket kezelő kódrészleteket ezért sokszor nehéz tesztelni, mivel elő kell állítani a kivételes helyzetet.
A labor verem osztályával könnyebb a helyzet; mivel a legtöbb helyen a sablon példányosító osztály, T
által generált kivételeket próbálja a verem helyesen kezelni. Nincs más dolgunk, mint létrehozni egy olyan osztályt (egy mock-ot),
amelynek adott művelete adott pillanatban kivételt dob, és ellenőrizni, hogy a verem objektum megoldotta-e a helyzetet.
Írj ilyen osztályt, és tesztelj vele pár helyzetet!
std::shared_ptr
Tudjuk, hogy egy dinamikusan foglalt objektumot („kezelt objektum”) egy std::shared_ptr
okos
pointernek adva az okos pointer felelni fog annak felszabadításáért. Sőt több std::shared_ptr
közösen is mutathat egyetlen kezelt objektumra. Sőt vannak std::weak_ptr
-ek is, amelyek azt is
tudják, hogy a kezelt objektum már delete-elve lett-e. Mindezt úgy oldják meg, hogy közösen használnak egy menedzser objektumot, amely
tartalmazza a kezelt objektum pointerét, továbbá a referenciaszámlálókat.
A menedzser objektumot a kellő számú indirekció és az élettartamának beállítása miatt szintén dinamikusan
kell foglalni. Ebből az következik, hogy az std::shared_ptr
konstruktora dinamikus
memóriafoglalást kell tartalmazzon, ami std::bad_alloc
típusú kivételhez vezethet. Ha a
konstruktora kivételt dob, akkor az okos pointer nem jött létre, tehát a destruktora sem fog lefutni, amelyik
felszabadította volna a kezelt objektumot.
A kérdés a következő. Helyesen működik-e a következő függvény akkor, ha a kezelt T
objektumot már sikerült
létrehozni, de a std::shared_ptr
konstruktorában kivétel keletkezik? Lesz-e itt memóriaszivárgás? Miért?
Rövid, vázlatos kódrészleteket írj a magyarázat mellé!
template <typename T>
std::shared_ptr<T> my_make_shared() {
return std::shared_ptr<T>(new T());
}
Megoldás
A shared_ptr
konstruktora elkapja a menedzser objektum létrehozásakor keletkező kivételt.
Ilyenkor a neki adott T
objektumot felszabadítja, és továbbdobja ezt a kivételt.
Más szóval, amint elindul a konstruktor, onnantól kezelt objektumnak számít a T
, nem pedig
csak onnantól kezdve, amikor lefutott. A konstruktor belseje valami ilyesmi:
template <typename T>
shared_ptr<T>::shared_ptr(T *rawptr) {
try {
this->managerobj = new managerobj;
} catch (...) { /* ha nem sikerült lefoglalni a menedzsert */
delete rawptr; /* akkor felszabadíŧja a kezelt objektumot */
throw;
}
this->managerobj->rawptr = rawptr;
this->managerobj->refcount = 1;
}
goto
:)
C-ben nincsenek kivételek. Ellenben sok olyan függvényt írunk, amelyik felépít valamilyen állapotot, erőforrásokat foglal egymás után (nyitott fájl, dinamikus memória stb.), és kivételes körülmény esetén ezeket fel kell szabadítania.

A kritikus függvényeknek két kilépési pontja (return) van: egy olyan, ahol hiba esetén fejeződik be a függvény, és egy olyan,
ahol normál esetben. Hiba esetén a lefoglalt erőforrásokat a foglaláshoz képest fordított sorrendben fel kell szabadítani, ahogy
a stack unwinding is történne C++-ban. Ezt úgy szokták megoldani, hogy a rendes kilépési pont alá teszik ezeket a
programrészeket, címkékkel ellátva. Így a felszabadítást végző programrészbe egy goto
utasítással be lehet ugrani,
az annak megfelelő helyre, ameddig a foglalásokkal eljutottunk. Így:
ErrorCode func() {
int* ints = (int*) malloc(sizeof(int) * 100);
if (ints == NULL)
goto end_nothing;
/* ... */
if (/* ... */)
goto end_ints;
/* ... */
free(ints);
return OK;
end_ints:
free(ints);
end_nothing:
return Fail;
}
3 3 3.4 9.2 0 1.2 0 4.5 -3 5.6 11
Írd át ilyenre az innen letölthető mátrixos programot: matrix.c! Ennek feladata az, hogy a
jobb oldalt látható formátumú fájlból beolvasson egy mátrixot, és visszaadja az arra mutató double**
pointert.
Megoldás
double **read_matrix(char const *filename) {
int w, h;
FILE *fp;
double ** ret;
fp = fopen(filename, "rt");
if (fp == NULL)
goto error;
if (fscanf(fp, "%d %d", &w, &h) != 2)
goto error_file;
/* mátrix foglalása */
ret = (double **) malloc(sizeof(double*) * h);
if (ret == NULL)
goto error_file;
/* kitöltjük null pointerrel, hogy tudni lehessen, melyik sor van lefoglalva */
for (int y = 0; y < h; ++y)
ret[y] = NULL;
for (int y = 0; y < h; ++y) {
ret[y] = (double *) malloc(sizeof(double) * w);
if (ret[y] == NULL)
goto error_matrix;
}
for (int y = 0; y < h; ++y) {
for (int x = 0; x < w; ++x) {
if (fscanf(fp, "%lf", &ret[y][x]) != 1)
goto error_matrix;
}
}
/* rendes visszatérés */
fclose(fp);
return ret;
/* hibák esetén erőforrások felszabadítása */
error_matrix:
for (int y = 0; y < h; ++y)
free(ret[y]);
free(ret);
error_file:
fclose(fp);
error:
return NULL;
}