Kivételek
Czirkos Zoltán · 2025.09.02.
Kivételkezelés
A laborokhoz
A laborok mellé minden héten lesz kiírva egy beadandó az admin portálon. Ide óra végén töltsd fel a forráskódokat (*.cpp, *.h)! A feladatokat ezért külön projektben oldd majd meg, ne írd felül a megoldásokat.
Labor otthoni munkában
A labor teljesítéséhez legalább az első két feladatot meg kell oldani.
- Rejtett komplexitás: a megoldásokat kommentben írd a kód mellé, magyarázatokkal.
- Kivételek a veremben: a kódot ne add be teszteletlenül! Figyelj arra, hogy a sablon kódnak csak a használt része példányosodik; hívd meg a tesztben mindegyik függvényt! Előbb dolgozatsz egész számokkal, de később próbáld ki a vermet úgy is, hogy „igazi” munkát is végez a beszúrás és a törlés; konstruktorral, destruktorral rendelkező osztállyal példányosítsd a vermet! Lehet ez
std::string, vagy az extrák alatt elérhető SuperNoisy.- Bináris fa: tesztelj is, mint az előzőnél!
Adott az alábbi kódrészlet. Minden további információ nélkül (csak ezt a részét ismerjük a programnak), a kérdés: a végrehajtása hány különböző vezérlési utat eredményezhet?
String EvaluateSalaryAndReturnName(Employee e) {
if (e.Title() == "CEO" || e.Salary() > 100000) {
cout << e.First() << " " << e.Last()
<< " is overpaid" << endl;
}
return e.First() + " " + e.Last();
}
A legszembetűnőbb természetesen az if() utasítás: vagy végrehajtódik
a kiírás, vagy nem – ez az elágazás eleve két lehetséges végrehajtási utat ad. Hol van a többi
lehetséges út, ha nem keletkezik kivétel, és akkor, ha igen?
Tipp
Az alábbiakra gondolj:
- Vezérlési utasítással megadott utak.
- Rövidzár tulajdonsággal rendelkező operátorok.
- A programozó által definiált operátorok.
- Sikertelen konstruktorhívások (pl. érték szerinti visszatérés).
- Sikertelen I/O műveletek.
Megoldás
Ez a feladat a GotW #20 fordítása.
Ha nem dobódik kivétel:
1. Ha e.Title() == "CEO", akkor a kifejezés második fele nem értékelődik ki, azaz e.Salary() sosem
hívódik meg, a || operátor rövidzár-tulajdonsága miatt.
2. Ha e.Title() != "CEO", de e.Salary() > 100000, akkor mindkét részkifejezés kiértékelődik, és a kiírás
végrehajtódik.
3. Ha e.Title() != "CEO" és e.Salary() <= 100000, a kiírás nem hajtódik végre.
A többi végrehajtási út a kivételek miatt jöhet létre:
*. Az argumentumot érték szerint kapja a függvény. Ehhez egy másolásra van szükség; a másoló konstruktor kivételt dobhat, bár ez
még a függvényen kívül történik. (Megj.: az eredeti írásban ez hibásan szerepel. Nem a hívott hozza létre ezt a másolatot, hanem a
hívó. Lásd a szabványt: 8.5.1.2(4) When a function is called, each parameter shall be initialized with its corresponding
argument. [...] The initialization and destruction of each parameter occurs within the context of the calling function.)
4. A visszatérési érték létrehozásához egy konstruktorhívásra van szükség. A String konstruktora kivételt
dobhat, amelyik egy másik úton jelent visszatérést.
5. A Title() tagfüggvény kivételt dobhat (semmit nem tudunk róla), vagy érték szerint adhat vissza objektumot (pl.
String), amely létrehozása kivételt eredményezhet.
6. Az operator== hívásához előfordulhat, hogy a "CEO" sztring literálist ideiglenes objektummá kell alakítani (pl. operator==(String, String) esetén operator==(String, String(char const*))). A temporális objektum létrehozása kivételt eredményezhet.
7. Az operator== maga is dobhat kivételt, ha nem beépített operátorról, hanem egy függvényről
van szó. (Fura helyzet lenne, de az elvi lehetőség megvan.)
8. Az 5-öshöz hasonlóan, a Salary() hívás kivételt eredményezhet.
9. A 6-oshoz hasonlóan temporális objektum keletkezhet, hogy megfelelő operator>-t lehessen találni.
10. Szintén extrém eset, ha az operator> furcsa működésű, programozó által írt függvény, kivételt
dobhat.
11. A || operátor is lehet olyan, amit a programozó írt (nem a beépített), és ez is dobhat kivételt.
12-16. Mind az öt << kiírás operátor dobhat kivételt.
17-18. A First() és Last() függvények kivételt dobhatnak a kiírás előtt.
19-20. Az 5-öshöz hasonlóan, a return utasításnál a First() és Last() függvények, vagy az azokból való visszatérés kivételt eredményezhet.
21. A sztringek összeadásához a " " literálist objektummá kellhet alakítani (pl. operator+(String, String) esetén).
22-23. Az operator+-ok is dobhatnak kivételt, az utolsó sorban.
Adott az alábbi törzzsel rendelkező verem osztály, amelynek teljes kódját töltsd le a feladat megoldásához: stack.cpp!
template <typename T>
class Stack {
public:
explicit Stack(size_t max_size);
Stack(Stack const &orig);
Stack & operator=(Stack const &orig) = delete;
~Stack();
void push(T const &what);
T pop();
bool empty() const;
private:
size_t size_;
size_t max_size_;
T *pData_;
};
Ez egy régebbi laborfeladat megoldása; egy egyszerű vermet valósít meg, placement new-s memóriakezeléssel, konstruktorban megadott méretű tömbbel.
Figyeld meg kivételkezelés szempontjából a kódot! Haladj sorban a függvényeken, és javítsd ki úgy őket, hogy a hibajelzésre kivételeket használjanak, és kivételek esetén pedig minél erősebb garanciát adjanak a helyes működésre! Vajon mit kell változtatni a...
- Konstruktoron?
Megoldás
A mostani változat nem kezeli a memóriafoglalási hibát (
malloc()null pointert ad). Ezt kezelni kell a visszaadott érték ellenőrzésével, vagy::operator new(size_t)használatával. Az utóbbi esetben az összes helyen át kell térni amalloc()/free()párosról az::operator new()/::operator delete()párosra! - Másoló konstruktoron?
Megoldás
A
malloc()mint az előző esetben. Ezen felül, bármelyikTkonstruktor kivételt dobhat. A másoló konstruktor megoldható konstruktor delegálással is, a mintamegoldásban egy ilyen verzió is van. - Destruktoron?
Megoldás
Remélhetőleg semmit. A
T-k destruktora ne dobjon kivételt. - A
push()függvényen?Megoldás
Először is, ha megtelt a verem, dobjon kivételt, ahelyett, hogy túlírná a foglalt memóriaterületet. Másodszor, a
size_tagváltozót csak azután növeljük meg, miután már sikerült a kapott elemet bemásolni a verembe! Így automatikusan változatlan marad majd a számláló (és ezzel együtt a verem is), ha aTmásolása nem sikerült. - A
pop()függvényen? Vigyázat, ez kemény dió.Megoldás
Első körben megcsinálhatjuk ugyanazokat a változtatásokat, mint a
push()függvényen. Asavedváltozóba akár mozgathatjuk is az adatot, mivel a következő sorban úgyis a destruktort hívjuk, egy garantáltan sikeres függvényt.template <typename T> T Stack<T>::pop() { if (size_ == 0) throw std::length_error("stack ures"); T saved(std::move(pData_[size_-1])); pData_[size_-1].~T(); size_--; return saved; }Ez nem rossz, de ilyen formán ez csak alap garanciát (basic guarantee) tud adni a hívónak. Miután a tömbből már kitörlődött az adat (a
pData_-n belüli példány destruktora lefutott), kezelt elemről még egy másolatot (vagy mozgatott példányt) kell készíteni. Ez lesz a visszatérési érték. Ha ennek a létrehozása nem sikerül areturn savedsorban, akkor kivétel fog keletkezni. A verem ilyenkor ugyan konzisztens állapotban van, de a kivett elemet a hívó nem kapja meg.Ezt a problémát sajnos lehetetlen megoldani. Nem lehetséges az, hogy a függvény ki is veszi az elemet a veremből, vissza és adja, és közben még erős garanciát is ad a művelet elvégzésére. Úgy lehetséges csak erős garanciát adni, ha két külön függvényt írunk: a
top()visszaadja a legfölső elemet (akár referencia szerint), de nem szedi ki a veremből; apop()eldobja a legfölső elemet, de nem tér vissza vele. Nem véletlen, hogy azstd::stacktop()éspop()függvénye is ilyen. - Az
empty()függvényen?Megoldás
Semmit, nincs benne kritikus művelet, csak egy
intvizsgálata és egyboolvisszatérési érték másolása. - A főprogramon?
Megoldás
Kapjuk el a kivételt.
- Mit kell ahhoz csinálni, hogy legyen másoló értékadó operátor?
Megoldás
Legegyszerűbb copy-and-swap-et használni, az jól kezeli a kivételeket is.
A teljes megoldás
A teljes megoldás: stack_megoldas.cpp.
Adott egy bináris fát megvalósító osztály, az alábbi kódrészlettel:
template <typename T>
class Node {
private:
T data;
Node* left = nullptr;
Node* right = nullptr;
public:
Node(Node const& original) : data(original.data) {
left = new Node(*original.left);
right = new Node(*original.right);
}
};
Mi a probléma ezzel a másoló konstruktorral? Javítsd meg! Írd meg a mozgató konstruktort is, továbbá a kétféle értékadó operátort és a destruktort! Figyelj arra, mikor, milyen kivételek keletkezhetnek!
Megoldás
Először is, nullptr-t nem szabadna dereferálni, kell báziskritérium a rekurzióhoz.
Az adattag másolása az inicializáló listán atomi művelet. Ha az létrejött, és utána kivétel dobódik, akkor az adattag destruktora meg fog hívódni automatikusan.
Ezért mi csak a dinamikus részért felelünk. Az egyes részfák másolása szintén atomi
művelet; new Node(...) vagy sikerül, és akkor létrejött a részfa, vagy nem, akkor
pedig a megfelelő destruktorok már hívódtak. Gond csak ott lehet, ha a bal részfa már megvan, és a jobb
oldali másolása sikertelen. Ezért ott el kell kapni a kivételt:
Node::Node(Node const& original) : data(original.data) {
if (original.left)
left = new Node(*original.left);
try {
if (original.right)
right = new Node(*original.right);
} catch (...) {
delete left;
throw;
}
}
Persze az sem lenne baj, ha mindkét new bekerülne a try blokkba,
és a catch-ben pedig két delete is szerepelne. A pointerek a fenti
osztálydeklarációban külön is inicializálva vannak, nullptr értékűek.
Nagyban megkönnyítjük a dolgunkat, ha nyers pointerek helyett okos pointereket használunk.
Ilyenkor ha valamelyik make_unique-on belüli new kivételt dob,
a unique_ptr-ek destruktora futni fog, ahogy a data-é is:
template <typename T>
class Node {
private:
T data;
std::unique_ptr<Node> left, right;
public:
Node(Node const& original) : data(original.data) {
if (original.left)
left = std::make_unique<Node>(*original.left);
if (original.right)
right = std::make_unique<Node>(*original.right);
}
};
A mozgató konstruktor a unique_ptr-es esetben:
Node::Node(Node && original)
: data(std::move(original.data))
, left(std::move(original.left))
, right(std::move(original.right))
{
}
Tehát igazából csak ennyi:
Node::Node(Node && original) = default;
Nyers pointerek esetén ez sem lenne ilyen egyszerű.