Sari la conținut

Excepții: arunca, încearcă, prinde

Înțelegeți mecanismul de excepție C++ — derularea stivei, ierarhia excepțiilor standard, excepțiile personalizate și când să preferați excepțiile decât codurile de eroare.

De ce există excepții

Înainte de excepții, codul C semnala erori prin returnarea unor valori speciale (cum ar fi-1sauNULL). Aceasta a avut un defect critic: apelanții puteau ignora în tăcere eroarea. Excepțiile rezolvă acest lucru forțând programul fie să gestioneze eroarea, fie să se încheie. De asemenea, ele separă site-ul de detecție a erorilor (adânc în codul bibliotecii) de site-ul de tratare a erorilor (în codul aplicației), care poate fi separat de mai multe cadre de apel.

Excepțiile C++ urmează un model de „cost zero pe calea succesului” pe majoritatea compilatoarelor moderne (folosind derularea bazată pe tabele). Când nu se aruncă nicio excepție, nu există nicio suprasarcină de rulare — nu există verificăriifascunse, nicio predicție de ramificație ratată. Costul este plătit numai atunci când se aruncă o excepție, ceea ce face ca excepțiile să fie ideale pentru condiții de eroare exceptionale (rare).

arunca, încearcă și prinde

Mecanismul de bază este simplu: aruncă un obiect pentru a semnala o eroare, împachetează cod riscant într-un bloc try și catch care se potrivesc cu tipurile de excepții.

divide.cpp
#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";
    }
}

Derularea stivei în detaliu

Când se aruncă o excepție, timpul de execuție efectuează derularea stivei: merge înapoi în stiva de apeluri, distrugând toate variabilele locale din fiecare cadru apelând destructorii acestora. Acesta este motivul pentru care RAII (Resource Acquisition Is Initialization) este atât de esențial - dacă dețineți resurse în pachetele RAII, acestea sunt eliberate automat în timpul derulării.

Procesul de desfășurare funcționează după cum urmează:

1. Expresiathrowcreează obiectul excepție (de obicei pe o zonă specială de memorie, nu pe stiva).
2. Runtime caută în funcția curentă un handlercatchcare se potrivește.
3. Dacă nu se găsește niciunul, se iese din funcția curentă, distrugând toate obiectele locale în ordine inversă construcției.
4. Se repetă pașii 2-3 în susul stivei de apeluri.
5. Dacă nu este găsit niciun handler în niciun cadru, este apelatstd::terminate().

Regulă cheie: aruncare după valoare, prindere după referințăconst. Aruncarea după valoare evită indicatoarele atârnate. Prinderea prin referințăconstevită tăierea (unde o excepție derivată este copiată într-un obiect de clasă de bază, pierzând datele derivate) și evită copiile inutile.

Ierarhia standard de excepție

Biblioteca standard oferă o ierarhie înrădăcinată lastd::exception. Înțelegerea acestuia vă ajută să aruncați tipul potrivit și să prindeți la nivelul potrivit.

hierarchy.cpp
// 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';
    }
}

Clase de excepție personalizate

Derive fromstd::exceptionor one of its subclasses to create domain-specific exceptions. Always overridewhat()to provide a meaningful message.

custom_exception.cpp
#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 și Specificații de excepție

Specificatorul noexcept le spune compilatorului (și cititorilor umani) că o funcție nu va arunca niciodată o excepție. Dacă o funcțienoexceptse aruncă,std::terminate()este apelat imediat - nu există nicio derulare a stivei.

De ce conteazănoexcept?

- Performanță: compilatorul poate omite tabelele de derulare pentru funcțiilenoexcept, producând cod mai strict.
- Optimizare STL: Containerele precumstd::vectorvor muta elemente în timpul realocării numai dacă constructorul de mutare estenoexcept. În caz contrar, aceștia revin la copiere pentru a menține garanția puternică de excepție. Aceasta poate fi o diferență dramatică de performanță.
- Documentație: Este un contract cu apelanții pe care funcția nu poate eșua.

Utilizaținoexceptpe: mutați constructori, mutați operatori de atribuire, funcții de schimb, destructori (implicitnoexcept), gettere simpli și operatori de comparație.

Vechea specificațiethrow()(șithrow(type-list)) a fost depreciată în C++11 și eliminată în C++20. Utilizați întotdeaunanoexceptîn schimb.

Capcană

Nu aruncați niciodată excepții în destructori. În timpul derulării stivei de la o altă excepție, lansarea unei a doua excepții apeleazăstd::terminate(). Dacă un destructor trebuie să efectueze o operație care poate eșua, prindeți excepția din interiorul destructorului și gestionați-o acolo (de exemplu, înregistrați și suprimați).

  • Prinsarea după valoare tranșează excepțiile derivate - întotdeauna prind deconst&
  • Aruncarea indicatoarelor (de exemplu,throw new std::runtime_error(...)) duce la scurgeri de memorie - aruncați după valoare
  • Prinderea...mai întâi ascunde toți gestionatorii specifici de sub el - pune-l pe ultimul
  • Excepțiile în destructori în timpul derulării stivei cauzeazăstd::terminate()
  • Folosirea excepțiilor pentru fluxul de control (căi fără eroare) înfrânge modelul cu costuri zero și dăunează performanței
Idei cheie
  • Aruncă după valoare, prinde prin referințăconstpentru a evita tăierea și scurgerile
  • Derularea stivei distruge obiectele locale în ordine inversă de construcție — RAII asigură curățarea
  • Excepțiile C++ au costuri zero pe calea succesului (desfășurare pe bază de tabel), costisitoare doar atunci când sunt aruncate
  • noexceptpermite optimizările compilatorului și este critic pentru operațiunile de mutare utilizate de STL
  • Obțineți excepții personalizate de lastd::runtime_errorsaustd::logic_errorși înlocuițiwhat()

Test — Verifică-ți cunoștințele

(15 XP)

1. De ce ar trebui să prindeți excepții prin`const&`, mai degrabă decât după valoare?

2. Ce se întâmplă dacă o funcție marcată`noexcept`aruncă o excepție?

3. În timpul derulării stivei, ce se întâmplă cu variabilele locale din fiecare cadru de stivă?