Εξαιρέσεις: ρίξε, δοκίμασε, πιάσε
Κατανοήστε τον μηχανισμό εξαιρέσεων C++ — ξετύλιγμα στοίβας, την τυπική ιεραρχία εξαιρέσεων, προσαρμοσμένες εξαιρέσεις και πότε να προτιμάτε τις εξαιρέσεις έναντι των κωδικών σφαλμάτων.
Γιατί υπάρχουν Εξαιρέσεις
Πριν από εξαιρέσεις, ο κωδικός C σηματοδοτούσε σφάλματα επιστρέφοντας ειδικές τιμές (όπως-1ήNULL). Αυτό είχε ένα κρίσιμο ελάττωμα: οι καλούντες μπορούσαν σιωπηλά να αγνοήσουν το σφάλμα. Οι εξαιρέσεις το επιλύουν αναγκάζοντας το πρόγραμμα είτε να χειριστεί το σφάλμα είτε να τερματίσει. Διαχωρίζουν επίσης την τοποθεσία ανίχνευσης σφαλμάτων (σε βάθος στον κώδικα βιβλιοθήκης) από την τοποθεσία διαχείρισης σφαλμάτων (στον κώδικα εφαρμογής), η οποία μπορεί να απέχει πολλά πλαίσια κλήσεων.
Οι εξαιρέσεις C++ ακολουθούν ένα μοντέλο "μηδενικού κόστους στη διαδρομή επιτυχίας" στους περισσότερους σύγχρονους μεταγλωττιστές (χρησιμοποιώντας ξετύλιγμα βάσει πίνακα). Όταν δεν υπάρχει εξαίρεση, δεν υπάρχει δεν υπάρχει γενικός χρόνος εκτέλεσης — δεν υπάρχουν κρυφοί έλεγχοιif, δεν χάνονται προβλέψεις διακλάδωσης. Το κόστος καταβάλλεται μόνο όταν υπάρχει πραγματικά μια εξαίρεση, γεγονός που καθιστά τις εξαιρέσεις ιδανικές για εξαιρετικές (σπάνιες) συνθήκες σφάλματος.
πετάξτε, δοκιμάστε και πιάστε
Ο βασικός μηχανισμός είναι απλός: πετάξτε ένα αντικείμενο για να σηματοδοτήσει ένα σφάλμα, τυλίξτε τον επικίνδυνο κώδικα σε ένα μπλοκ try και catch αντίστοιχους τύπους εξαιρέσεων.
#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";
}
}Ξετύλιγμα στοίβας με λεπτομέρειες
Όταν δημιουργείται μια εξαίρεση, ο χρόνος εκτέλεσης εκτελεί ξετύλιξη στοίβας: ανεβάζει αντίστροφα τη στοίβα κλήσεων, καταστρέφοντας όλες τις τοπικές μεταβλητές σε κάθε πλαίσιο καλώντας τους καταστροφείς τους. Αυτός είναι ο λόγος για τον οποίο το RAII (Resource Acquisition Is Initialization) είναι τόσο σημαντικό — εάν κρατάτε πόρους σε περιτυλίγματα RAII, αυτοί απελευθερώνονται αυτόματα κατά την εκτύλιξη.
Η διαδικασία ξετύλιξης λειτουργεί ως εξής:
1. Η έκφρασηthrowδημιουργεί το αντικείμενο εξαίρεσης (συνήθως σε μια ειδική περιοχή της μνήμης, όχι στη στοίβα).
2. Ο χρόνος εκτέλεσης αναζητά την τρέχουσα λειτουργία για έναν αντίστοιχο χειριστήcatch.
3. Εάν δεν βρεθεί κανένα, βγαίνει από την τρέχουσα συνάρτηση, καταστρέφοντας όλα τα τοπικά αντικείμενα με αντίστροφη σειρά κατασκευής.
4. Επαναλαμβάνει τα βήματα 2-3 μέχρι τη στοίβα κλήσεων.
5. Εάν δεν βρεθεί κανένας χειριστής σε κανένα πλαίσιο, καλείται τοstd::terminate().
Βασικός κανόνας: ρίψη με βάση την τιμή, πιάσε με αναφοράconst. Η ρίψη με βάση την τιμή αποφεύγει τους δείκτες που κρέμονται. Η σύλληψη με αναφοράconstαποφεύγει τον τεμαχισμό (όπου μια παράγωγη εξαίρεση αντιγράφεται σε ένα αντικείμενο βασικής κλάσης, χάνοντας τα παραγόμενα δεδομένα) και αποφεύγονται τα περιττά αντίγραφα.
Η τυπική ιεραρχία εξαίρεσης
Η τυπική βιβλιοθήκη παρέχει μια ιεραρχία που βασίζεται στοstd::exception. Η κατανόησή του σε βοηθά να πετάξεις τον σωστό τύπο και να πιάσεις στο σωστό επίπεδο.
// 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';
}
}Προσαρμοσμένες τάξεις εξαίρεσης
Προκύπτουν από τοstd::exceptionή μία από τις υποκλάσεις του για τη δημιουργία εξαιρέσεων για συγκεκριμένους τομείς. Παρακάμπτετε πάντα τοwhat()για να παρέχετε ένα ουσιαστικό μήνυμα.
#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 και Exception Specifications
Ο προσδιοριστής noexcept λέει στον μεταγλωττιστή (και στους ανθρώπους αναγνώστες) ότι μια συνάρτηση δεν θα δημιουργήσει ποτέ εξαίρεση. Εάν μια συνάρτησηnoexceptεκτοξεύεται, τοstd::terminate()καλείται αμέσως — δεν υπάρχει ξετύλιγμα στοίβας.
Γιατί έχει σημασία τοnoexcept;
- Απόδοση: Ο μεταγλωττιστής μπορεί να παραλείψει τους πίνακες ξετυλίγματος για τις συναρτήσειςnoexcept, παράγοντας πιο αυστηρό κώδικα.
- Βελτιστοποίηση STL: Εμπορευματοκιβώτια όπως τοstd::vectorθα μετακινούν στοιχεία κατά την ανακατανομή μόνο εάν ο κατασκευαστής μετακίνησης είναιnoexcept. Διαφορετικά, επιστρέφουν στην αντιγραφή για να διατηρήσουν την ισχυρή εγγύηση εξαίρεσης. Αυτό μπορεί να είναι μια δραματική διαφορά απόδοσης.
- Τεκμηρίωση: Είναι μια σύμβαση με καλούντες ότι η λειτουργία δεν μπορεί να αποτύχει.
Χρησιμοποιήστε τοnoexceptσε: μετακίνηση κατασκευαστών, μετακίνηση τελεστών εκχώρησης, συναρτήσεις ανταλλαγής, καταστροφείς (σιωπηράnoexcept), απλούς λήπτες και τελεστές σύγκρισης.
Η παλιά προδιαγραφήthrow()(και τοthrow(type-list)) καταργήθηκε στο C++11 και καταργήθηκε στο C++20. Χρησιμοποιείτε πάντα τοnoexcept.
Ποτέ μην ρίχνετε εξαιρέσεις σε καταστροφείς. Κατά το ξετύλιγμα της στοίβας από μια άλλη εξαίρεση, η δημιουργία μιας δεύτερης εξαίρεσης καλεί τοstd::terminate(). Εάν ένας καταστροφέας πρέπει να εκτελέσει μια λειτουργία που μπορεί να αποτύχει, πιάστε την εξαίρεση μέσα στον καταστροφέα και χειριστείτε την εκεί (π.χ. καταγραφή και καταστολή).
- Η σύλληψη με βάση την τιμή προέρχεται από εξαιρέσεις — πάντα αλιεύουμε από το
const& - Η ρίψη δεικτών (π.χ.
throw new std::runtime_error(...)) οδηγεί σε διαρροές μνήμης — ρίψη βάσει τιμής - Η σύλληψη του
...πρώτα κρύβει όλους τους συγκεκριμένους χειριστές κάτω από αυτό — τοποθετήστε το τελευταίο - Εξαιρέσεις σε καταστροφείς κατά το ξετύλιγμα στοίβας προκαλούν το
std::terminate() - Η χρήση εξαιρέσεων για τη ροή ελέγχου (διαδρομές χωρίς σφάλματα) ακυρώνει το μοντέλο μηδενικού κόστους και βλάπτει την απόδοση
- Ρίξτε με βάση την τιμή, πιάστε από την αναφορά
constγια να αποφύγετε τον τεμαχισμό και τις διαρροές - Το ξετύλιγμα της στοίβας καταστρέφει τοπικά αντικείμενα με αντίστροφη σειρά κατασκευής — Το RAII εξασφαλίζει καθαρισμό
- Οι εξαιρέσεις C++ έχουν μηδενικό κόστος στη διαδρομή επιτυχίας (ξετύλιξη βάσει πίνακα), ακριβές μόνο όταν εκτινάσσονται
- Το
noexceptεπιτρέπει βελτιστοποιήσεις μεταγλωττιστή και είναι κρίσιμο για λειτουργίες μετακίνησης που χρησιμοποιούνται από το STL - Εξάγετε προσαρμοσμένες εξαιρέσεις από το
std::runtime_errorή τοstd::logic_errorκαι παρακάμπτετε τοwhat()
Κουίζ — Δοκιμάστε τις γνώσεις σας
(15 XP)1. Γιατί πρέπει να συλλαμβάνετε τις εξαιρέσεις από το`const&`και όχι από την τιμή;
2. Τι συμβαίνει εάν μια συνάρτηση με την ένδειξη`noexcept`κάνει εξαίρεση;
3. Κατά το ξετύλιγμα της στοίβας, τι συμβαίνει με τις τοπικές μεταβλητές σε κάθε πλαίσιο στοίβας;