Výjimky: hodit, zkusit, chytit
Pochopte mechanismus výjimek C++ – odvíjení zásobníku, standardní hierarchie výjimek, vlastní výjimky a kdy upřednostňovat výjimky před chybovými kódy.
Proč existují výjimky
Před výjimkami kód C signalizoval chyby vracením speciálních hodnot (jako-1neboNULL). To mělo kritickou chybu: volající mohli chybu tiše ignorovat. Výjimky to řeší tím, že nutí program buď zpracovat chybu, nebo ukončit. Oddělují také web detekce chyb (hluboce v kódu knihovny) od webu zpracování chyb (v kódu aplikace), který může být od sebe vzdálený o mnoho rámců volání.
Výjimky C++ se řídí modelem „nulových nákladů na cestě k úspěchu“ na většině moderních kompilátorů (používá odvíjení založené na tabulkách). Když není vyvolána žádná výjimka, nedochází k žádné režii za běhu – žádné skryté kontrolyif, žádné předpovědi větví nevynechány. Náklady se platí pouze v případě, že je výjimka skutečně vyvolána, což činí výjimky ideální pro výjimečné (vzácné) chybové stavy.
házet, zkoušet a chytat
Základní mechanismus je přímočarý: hoďte objekt pro signalizaci chyby, zabalte riskantní kód do bloku zkuste a zachyťte odpovídající typy výjimek.
#include <iostream>
#include <stdexcept>
#include <string>
double divide(double numerator, double denominator) {
if (denominator == 0.0) {
throw std::invalid_argument("denominator cannot be zero");
}
return numerator / denominator;
}
int main() {
try {
double result = divide(10.0, 0.0);
std::cout << "Result: " << result << '\n';
} catch (const std::invalid_argument& e) {
std::cerr << "Invalid argument: " << e.what() << '\n';
} catch (const std::exception& e) {
std::cerr << "Error: " << e.what() << '\n';
} catch (...) {
std::cerr << "Unknown error occurred\n";
}
}Stack Unwinding in Detail
Když je vyvolána výjimka, běhové prostředí provede rozbalení zásobníku: vrátí se zpět do zásobníku volání a zničí všechny lokální proměnné v každém rámci voláním jejich destruktorů. To je důvod, proč je RAII (Resource Acquisition Is Initialization) tak zásadní – pokud držíte zdroje v obalech RAII, jsou automaticky uvolněny během odvíjení.
Proces odvíjení funguje následovně:
1. Výrazthrowvytvoří objekt výjimky (obvykle ve speciální oblasti paměti, nikoli v zásobníku).
2. Runtime hledá v aktuální funkci odpovídající obslužný programcatch.
3. Pokud není žádný nalezen, ukončí aktuální funkci a zničí všechny místní objekty v opačném pořadí výstavby.
4. Opakuje kroky 2-3 v zásobníku volání.
5. Pokud není v žádném rámci nalezen žádný handler, zavolá sestd::terminate().
Klíčové pravidlo: házet podle hodnoty, chytat podle referenceconst. Házení podle hodnoty zabraňuje visícím ukazatelům. Chytání pomocí odkazuconstzabraňuje krájení (kde je odvozená výjimka zkopírována do objektu základní třídy, čímž dochází ke ztrátě odvozených dat) a zabraňuje zbytečným kopiím.
Hierarchie standardních výjimek
Standardní knihovna poskytuje hierarchii zakořeněnou vstd::exception. Jeho pochopení vám pomůže hodit správný typ a chytit na správné úrovni.
// Standard exception hierarchy (simplified)
//
// std::exception
// ├── std::logic_error (programmer mistakes - precondition violations)
// │ ├── std::invalid_argument
// │ ├── std::domain_error
// │ ├── std::length_error
// │ └── std::out_of_range
// ├── std::runtime_error (errors detectable only at runtime)
// │ ├── std::range_error
// │ ├── std::overflow_error
// │ ├── std::underflow_error
// │ └── std::system_error (OS-level errors with error_code)
// └── std::bad_alloc (memory allocation failure)
// std::bad_cast (failed dynamic_cast)
// std::bad_typeid (typeid on null pointer)
#include <stdexcept>
#include <vector>
#include <iostream>
int main() {
std::vector<int> v = {1, 2, 3};
try {
// .at() throws std::out_of_range, unlike operator[] which is UB
int val = v.at(10);
} catch (const std::out_of_range& e) {
std::cerr << "Out of range: " << e.what() << '\n';
}
}Vlastní třídy výjimek
Odvozením zstd::exceptionnebo jedné z jeho podtříd vytvoříte výjimky specifické pro doménu. Vždy přepištewhat(), abyste poskytli smysluplnou zprávu.
#include <stdexcept>
#include <string>
#include <iostream>
class FileParseError : public std::runtime_error {
public:
FileParseError(const std::string& filename, int line, const std::string& msg)
: std::runtime_error(
"Parse error in '" + filename + "' at line " +
std::to_string(line) + ": " + msg),
filename_(filename),
line_(line) {}
const std::string& filename() const noexcept { return filename_; }
int line() const noexcept { return line_; }
private:
std::string filename_;
int line_;
};
void parse_config(const std::string& filename) {
// Simulate a parse failure at line 42
throw FileParseError(filename, 42, "unexpected token '@@'");
}
int main() {
try {
parse_config("app.conf");
} catch (const FileParseError& e) {
std::cerr << e.what() << '\n';
std::cerr << " File: " << e.filename()
<< ", Line: " << e.line() << '\n';
}
}noexcept a specifikace výjimek
Specifikátor noexcept říká kompilátoru (a lidským čtenářům), že funkce nikdy nevyvolá výjimku. Pokud funkcenoexceptvyvolá, okamžitě se zavolástd::terminate()– nedochází k odvíjení zásobníku.
Proč nanoexceptzáleží?
- Výkon: Kompilátor může vynechat odvíjecí tabulky pro funkcenoexcepta vytvářet přísnější kód.
- STL optimalizace: Kontejnery jakostd::vectorbudou přesouvat prvky během realokace pouze v případě, že konstruktor přesunu jenoexcept. V opačném případě se vrátí ke kopírování, aby byla zachována silná záruka výjimek. To může být dramatický rozdíl ve výkonu.
- Dokumentace: Jedná se o smlouvu s volajícími, že funkce nemůže selhat.
Použijtenoexceptna: konstruktory přesunu, operátory přiřazení přesunu, swapovací funkce, destruktory (implicitněnoexcept), jednoduché getry a operátory porovnání.
Stará specifikacethrow()(athrow(type-list)) byla zastarána v C++11 a odstraněna v C++20. Vždy místo toho použijtenoexcept.
Nikdy nevyhazujte výjimky v destruktorech. Během odvíjení zásobníku od jiné výjimky vyvolá vyvolání druhé výjimkystd::terminate(). Pokud musí destruktor provést operaci, která může selhat, zachyťte výjimku uvnitř destruktoru a zpracujte ji tam (např. log a potlačení).
- Zachycování podle hodnoty výjimky odvozené od řezů – vždy zachycení podle
const& - Vyhození ukazatelů (např.
throw new std::runtime_error(...)) vede k únikům paměti – házení podle hodnoty - Chytit
...jako první skryje všechny konkrétní handlery pod sebou – dejte to jako poslední - Výjimky v destruktorech během odvíjení zásobníku způsobují
std::terminate() - Použití výjimek pro tok řízení (cesty bez chyb) maří model s nulovými náklady a snižuje výkon
- Hodit podle hodnoty, zachytit odkazem
const, aby se zabránilo krájení a únikům - Odvíjení stohu ničí místní objekty v opačném pořadí konstrukce — RAII zajišťuje vyčištění
- Výjimky C++ jsou nulové náklady na cestě k úspěchu (odvíjení založené na tabulkách), drahé pouze při vyvolání
noexceptumožňuje optimalizaci kompilátoru a je rozhodující pro operace přesunu používané STL- Odvozte vlastní výjimky z
std::runtime_errornebostd::logic_errora přepištewhat()
Kvíz — Otestujte své znalosti
(15 XP)1. Proč byste měli zachytit výjimky podle`const&`spíše než podle hodnoty?
2. What happens if a function marked`noexcept`throws an exception?
3. Co se během odvíjení zásobníku stane s lokálními proměnnými v každém rámci zásobníku?