Στρατηγικές χειρισμού σφαλμάτων: Επιλέγοντας τη σωστή προσέγγιση
Συγκρίνετε εξαιρέσεις, κωδικούς σφάλματος και std::expected — μάθετε πότε είναι κατάλληλη κάθε προσέγγιση και πώς να χειρίζεστε σφάλματα σε κατασκευαστές, καταστροφείς και κώδικα πολλαπλών νημάτων.
Καμία λύση που ταιριάζει σε όλους
Το C++ προσφέρει πολλαπλούς μηχανισμούς διαχείρισης σφαλμάτων, ο καθένας με διαφορετικούς ανταγωνισμούς. Η επιλογή του σωστού εξαρτάται από τον τομέα σας:
Οι εξαιρέσεις είναι καλύτερες για σφάλματα που είναι σπάνια και δεν μπορούν να αντιμετωπιστούν τοπικά. Έχουν μηδενικό κόστος στην πορεία επιτυχίας, αλλά είναι ακριβά όταν εκτοξεύονται (1000$ νανοδευτερόλεπτα). Διαδίδονται αυτόματα στα πλαίσια κλήσεων.
Οι κωδικοί σφάλματος (τιμές επιστροφής,errno,std::error_code) είναι οι καλύτεροι για σφάλματα που είναι αναμενόμενα και συχνά. Έχουν ένα μικρό, σταθερό κόστος σε κάθε κλήση (ο καλών πρέπει να ελέγξει την τιμή επιστροφής). Είναι το πρότυπο σε C API και κώδικα σε επίπεδο πυρήνα.
Το std::expected (C++23) συνδυάζει το καλύτερο και από τα δύο: αναγκάζει τον καλούντα να αναγνωρίσει το σφάλμα (όπως κωδικοί σφάλματος), αλλά φέρει έναν πλούσιο τύπο σφάλματος (όπως εξαιρέσεις) και υποστηρίζει μονοδική αλυσίδα. Γίνεται γρήγορα η προτιμώμενη προσέγγιση στον νέο κώδικα C++.
Η βασική ιδέα: αυτές δεν είναι ανταγωνιστικές φιλοσοφίες — είναι εργαλεία για διαφορετικές καταστάσεις και ο κώδικας παραγωγής χρησιμοποιεί συχνά και τις τρεις.
Σύγκριση: Exceptions vs Error Codes vs std::expected
| Κριτήριο | Εξαιρέσεις | Κωδικοί σφαλμάτων |std::expected|
|---|---|---|---|
| Κόστος επιτυχίας | Μηδέν (βάσει πίνακα) | Υποκατάστημα + επιταγή | Ελάχιστη (αξία αποθήκευσης) |
| Κόστος επί σφάλματος | Ψηλά (ξετύλιγμα) | Το ίδιο και η επιτυχία | Το ίδιο και η επιτυχία |
| Μπορεί να αγνοηθεί; | Όχι (το πρόγραμμα τερματίζεται) | Ναι (σιωπηλά σφάλματα) | Πιο δύσκολο (πρέπει να έχει πρόσβαση στο.value()) |
| Κατασκευαστές | Μόνο επιλογή | Δεν είναι δυνατή η επιστροφή τιμής | Δεν είναι δυνατή η επιστροφή τιμής |
| Καταστροφείς | Δεν πρέπει να πετάξει | Ωραία | Ωραία |
| Συνθεσιμότητα | Περιορισμένη (δοκιμάστε/πιάστε) | Χειροκίνητα if-chains | Μοναδικός (και_τότε, μετασχηματισμός) |
| Δυαδικό μέγεθος | Μεγαλύτερο (ξετύλιγμα τραπεζιών) | Minimal | Minimal |
| Ασφαλές σε πραγματικό χρόνο; | Όχι (απεριόριστος χρόνος) | Ναι | Ναι |
Αντιμετώπιση σφαλμάτων σε κατασκευαστές
Οι κατασκευαστές δεν μπορούν να επιστρέψουν κωδικούς σφάλματος — δεν έχουν τιμή επιστροφής. Οι εξαιρέσεις είναι ο μόνος καθαρός μηχανισμός για τη σηματοδότηση αστοχίας κατασκευής. Μια εξαίρεση σημαίνει ότι το αντικείμενο δεν κατασκευάστηκε ποτέ πλήρως, επομένως ο καταστροφέας του δεν καλείται ποτέ (αλλά καλούνται οι καταστροφείς ήδη κατασκευασμένων υπο-αντικειμένων και βάσεων).
#include <fstream>
#include <string>
#include <stdexcept>
#include <iostream>
class ConfigFile {
public:
explicit ConfigFile(const std::string& path)
: path_(path), stream_(path) {
if (!stream_.is_open()) {
throw std::runtime_error(
"Cannot open config file: " + path);
}
// Parse the file — if this throws, stream_ is
// already constructed and its destructor will
// close the file automatically (RAII)
parse();
}
std::string get(const std::string& key) const {
auto it = data_.find(key);
if (it == data_.end()) {
throw std::out_of_range("Key not found: " + key);
}
return it->second;
}
private:
void parse() {
std::string line;
while (std::getline(stream_, line)) {
auto pos = line.find('=');
if (pos != std::string::npos) {
data_[line.substr(0, pos)] = line.substr(pos + 1);
}
}
}
std::string path_;
std::ifstream stream_; // RAII: closed by destructor
std::map<std::string, std::string> data_;
};
// Usage: the object is fully valid or does not exist
int main() {
try {
ConfigFile cfg("/etc/myapp.conf");
std::cout << cfg.get("port") << '\n';
} catch (const std::exception& e) {
std::cerr << e.what() << '\n';
}
}std::error_code για σφάλματα επιπέδου συστήματος
Τοstd::error_codeπαρέχει έναν ελαφρύ μηχανισμό που δεν εκτοξεύεται για σφάλματα σε επίπεδο συστήματος. Η βιβλιοθήκηείναι ένα εξαιρετικό παράδειγμα — οι περισσότερες συναρτήσεις έχουν υπερφόρτωση που παίρνει μια παράμετροstd::error_code&αντί να ρίχνει.
#include <filesystem>
#include <iostream>
#include <system_error>
namespace fs = std::filesystem;
// Throwing version — suitable for unexpected errors
void copy_file_throwing(const fs::path& src, const fs::path& dst) {
fs::copy_file(src, dst); // throws fs::filesystem_error on failure
}
// Non-throwing version — suitable for expected failures
bool copy_file_safe(const fs::path& src, const fs::path& dst) {
std::error_code ec;
fs::copy_file(src, dst, ec); // sets ec instead of throwing
if (ec) {
std::cerr << "Copy failed: " << ec.message()
<< " (" << ec.category().name()
<< ':' << ec.value() << ")\n";
return false;
}
return true;
}
int main() {
// The error_code version is preferable in loops where
// failure is expected (e.g., batch file processing)
for (const auto& entry : fs::directory_iterator(".")) {
std::error_code ec;
auto sz = fs::file_size(entry.path(), ec);
if (!ec) {
std::cout << entry.path().filename()
<< ": " << sz << " bytes\n";
}
}
}Οι καταστροφείς στο C++11 και μεταγενέστερα είναι σιωπηράnoexcept. Εάν ένας καταστροφέας ρίξει, καλείταιstd::terminate(). Ακόμα κι αν επισημάνετε ρητά έναν καταστροφέαnoexcept(false), η ρίψη κατά την εκτύλιξη της στοίβας (όταν μια άλλη εξαίρεση είναι ήδη ενεργή) καλεί τοstd::terminate().
- Καταγραφή και καταστολή: Εάν μια λειτουργία εκκαθάρισης αποτύχει, καταγράψτε το σφάλμα και συνεχίστε
- Παρέχετε μια ξεχωριστή μέθοδο
close(): Αφήστε τους καλούντες να χειριστούν ρητά τα σφάλματα καθαρισμού πριν από την καταστροφή - Σχεδιάστε πόρους ώστε η εκκαθάριση να μην μπορεί να αποτύχει: Για παράδειγμα, το κλείσιμο ενός περιγραφέα αρχείου είναι πάντα επιτυχές σε επίπεδο λειτουργικού συστήματος
- Ποτέ μην καλείτε μια συνάρτηση ρίψης από έναν καταστροφέα χωρίς να την τυλίξετε σε δοκιμή/πιάσιμο
Χειρισμός σφαλμάτων σε κώδικα πολλαπλών νημάτων
Οι εξαιρέσεις δεν ξεπερνούν τα όρια του νήματος. Εάν ένα νήμα εκτιναχθεί και η εξαίρεση δεν περιλαμβάνεται σε αυτό το νήμα, καλείται τοstd::terminate(). Για να διαδώσετε εξαιρέσεις μεταξύ νημάτων:
1. Χρησιμοποιήστε τα std::future και std::async: Οι εξαιρέσεις που εμφανίζονται στην ασύγχρονη εργασία καταγράφονται και επαναλαμβάνονται όταν καλείτε το.get()στο μέλλον.
2. Χρησιμοποιήστε το std::exception_ptr με ταstd::current_exception()καιstd::rethrow_exception()για μη αυτόματη καταγραφή και μεταφορά εξαιρέσεων.
3. Χρησιμοποιήστε το std::promise::set_exception() για να στείλετε μια εξαίρεση από ένα νήμα παραγωγού σε έναν καταναλωτή.
Για ταυτόχρονο κώδικα υψηλής απόδοσης, συχνά προτιμώνται οι κωδικοί σφάλματος ή τοstd::expected, επειδή αποφεύγουν το κόστος της υποδομής εξαίρεσης σε κάθε νήμα και είναι πιο εύκολο να το αιτιολογήσουν πέρα από τα όρια νημάτων.
Αντιστοιχίστε τον μηχανισμό χειρισμού σφαλμάτων με την κατάσταση.
- Χρησιμοποιήστε εξαιρέσεις για σφάλματα που είναι σπάνια, δεν μπορούν να αντιμετωπιστούν τοπικά και θα πρέπει να διαδοθούν στη στοίβα (π.χ., το αρχείο δεν βρέθηκε, η βάση δεδομένων προς τα κάτω)
- Χρησιμοποιήστε κωδικούς σφάλματος για λειτουργίες υψηλής συχνότητας όπου η αποτυχία είναι φυσιολογική και αναμενόμενη (π.χ. παράλειψη αναζήτησης προσωρινής μνήμης, μη αποκλεισμός I/O θα αποκλειστεί)
- Χρησιμοποιήστε το
std::expectedγια νέο κώδικα όπου θέλετε ρητό χειρισμό σφαλμάτων με πλούσιους τύπους σφαλμάτων και μοναδική σύνθεση - Χρησιμοποιήστε εξαιρέσεις σε κατασκευαστές — είναι ο μόνος τρόπος για να σηματοδοτήσετε την αποτυχία πριν την ύπαρξη του αντικειμένου
- Μην ρίχνετε ποτέ καταστροφείς — καταγράψτε το σφάλμα και καταργήστε το
- Οι εξαιρέσεις, οι κωδικοί σφαλμάτων και το
std::expectedείναι συμπληρωματικά εργαλεία — όχι ανταγωνιστικές φιλοσοφίες - Οι κατασκευαστές πρέπει να χρησιμοποιούν εξαιρέσεις για την αστοχία σήματος. Οι κωδικοί σφάλματος δεν αποτελούν επιλογή
- Οι καταστροφείς δεν πρέπει ποτέ να ρίχνουν — είναι σιωπηρά
noexceptαπό το C++11 - Οι εξαιρέσεις δεν υπερβαίνουν τα όρια νημάτων — χρησιμοποιήστε
std::futureήstd::exception_ptrγια να τις μεταφέρετε - Το
std::error_codeείναι ιδανικό για σφάλματα σε επίπεδο συστήματος όπου θέλετε ένα API χωρίς ρίψη
Κουίζ — Δοκιμάστε τις γνώσεις σας
(15 XP)1. Γιατί οι εξαιρέσεις είναι ο προτιμώμενος μηχανισμός χειρισμού σφαλμάτων στους κατασκευαστές;
2. Τι συμβαίνει εάν μια εξαίρεση πέσει σε έναν καταστροφέα κατά την εκτύλιξη στοίβας από μια άλλη εξαίρεση;
3. Πώς μπορούν να μεταφερθούν οι εξαιρέσεις μεταξύ των νημάτων;