Μετάβαση στο περιεχόμενο

Στρατηγικές χειρισμού σφαλμάτων: Επιλέγοντας τη σωστή προσέγγιση

Συγκρίνετε εξαιρέσεις, κωδικούς σφάλματος και 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 |
| Ασφαλές σε πραγματικό χρόνο; | Όχι (απεριόριστος χρόνος) | Ναι | Ναι |

Αντιμετώπιση σφαλμάτων σε κατασκευαστές

Οι κατασκευαστές δεν μπορούν να επιστρέψουν κωδικούς σφάλματος — δεν έχουν τιμή επιστροφής. Οι εξαιρέσεις είναι ο μόνος καθαρός μηχανισμός για τη σηματοδότηση αστοχίας κατασκευής. Μια εξαίρεση σημαίνει ότι το αντικείμενο δεν κατασκευάστηκε ποτέ πλήρως, επομένως ο καταστροφέας του δεν καλείται ποτέ (αλλά καλούνται οι καταστροφείς ήδη κατασκευασμένων υπο-αντικειμένων και βάσεων).

constructor_error.cpp
#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&αντί να ρίχνει.

error_code.cpp
#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. Πώς μπορούν να μεταφερθούν οι εξαιρέσεις μεταξύ των νημάτων;