Strategii de tratare a erorilor: alegerea abordării corecte
Comparați excepțiile, codurile de eroare și std::expected — aflați când fiecare abordare este adecvată și cum să gestionați erorile din constructori, destructori și coduri cu mai multe fire.
Nicio soluție unică pentru toate
C++ oferă mai multe mecanisme de gestionare a erorilor, fiecare cu compromisuri diferite. Alegerea celui potrivit depinde de domeniul dvs.:
Excepțiile sunt cele mai bune pentru erorile care sunt rare și nu pot fi gestionate local. Au costuri zero pe calea succesului, dar sunt scumpe atunci când sunt aruncate (1000 de dolari nanosecunde). Acestea se propagă automat în cadrele de apel.
Codurile de eroare (valori returnate,errno,std::error_code) sunt cele mai bune pentru erorile care sunt așteptate și frecvente. Au un cost mic, constant la fiecare apel (apelantul trebuie să verifice valoarea returnată). Sunt standardul în API-urile C și codul la nivel de kernel.
std::expected (C++23) combină cele mai bune dintre ambele: forțează apelantul să recunoască eroarea (cum ar fi codurile de eroare), dar poartă un tip de eroare bogat (cum ar fi excepțiile) și acceptă înlănțuirea monadică. Devine rapid abordarea preferată în noul cod C++.
Perspectiva cheie: acestea nu sunt filozofii concurente - sunt instrumente pentru diferite situații, iar codul de producție le folosește adesea pe toate trei.
Comparație: excepții vs coduri de eroare vs std::expected
| Criterion | Exceptions | Error Codes |std::expected|
|---|---|---|---|
| Cost on success | Zero (table-based) | Branch + check | Minimal (stores value) |
| Cost on error | High (unwinding) | Same as success | Same as success |
| Can be ignored? | No (program terminates) | Yes (silent bugs) | Harder (must access.value()) |
| Constructors | Only option | Cannot return a value | Cannot return a value |
| Destructors | Must not throw | Fine | Fine |
| Composability | Limited (try/catch) | Manual if-chains | Monadic (and_then, transform) |
| Binary size | Larger (unwind tables) | Minimal | Minimal |
| Real-time safe? | No (unbounded time) | Yes | Yes |
Tratarea erorilor în constructori
Constructorii nu pot returna coduri de eroare - nu au valoare returnată. Excepțiile sunt singurul mecanism curat pentru semnalizarea eșecului construcției. O excepție aruncată înseamnă că obiectul nu a fost niciodată complet construit, astfel încât destructorul său nu este niciodată numit (dar sunt numiți destructori ai subobiectelor și bazelor deja construite).
#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 pentru erori la nivel de sistem
std::error_codeoferă un mecanism ușor, care nu aruncă erorile la nivel de sistem. Bibliotecaeste un exemplu grozav - majoritatea funcțiilor au o supraîncărcare care ia un parametrustd::error_code&în loc să fie aruncat.
#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";
}
}
}Destructorii din C++11 și mai târziu sunt implicitnoexcept. Dacă un destructor aruncă, este apelatstd::terminate(). Chiar dacă marcați în mod explicit un destructornoexcept(false), aruncarea în timpul derulării stivei (când o altă excepție este deja activă) apeleazăstd::terminate().
- Înregistrați și suprimați: dacă o operațiune de curățare eșuează, înregistrați eroarea și continuați
- Oferiți o metodă
close()separată: permiteți apelanților să gestioneze în mod explicit erorile de curățare înainte de distrugere - Proiectați resurse astfel încât curățarea să nu poată eșua: De exemplu, închiderea unui descriptor de fișier are întotdeauna succes la nivelul sistemului de operare
- Nu apelați niciodată o funcție de aruncare de la un destructor fără a o înfășura într-un try/catch
Tratarea erorilor în codul cu mai multe fire
Excepțiile nu depășesc granițele firelor. Dacă un fir de execuție aruncă și excepția nu este prinsă în acel fir de execuție, este apelatstd::terminate(). Pentru a propaga excepțiile între fire:
1. Utilizați std::future și std::async: excepțiile aruncate în sarcina asincronă sunt capturate și aruncate din nou atunci când apelați.get()pe viitor.
2. Utilizați std::exception_ptr custd::current_exception()șistd::rethrow_exception()pentru a captura și transporta manual excepțiile.
3. Folosiți std::promise::set_exception() pentru a trimite o excepție de la un fir de execuție producător către un consumator.
Pentru codul concurent de mare debit, codurile de eroare saustd::expectedsunt adesea preferate, deoarece evită costul infrastructurii de excepție în fiecare fir și sunt mai ușor de raționat peste granițele firului.
Potriviți mecanismul de gestionare a erorilor la situație.
- Use exceptions for errors that are rare, cannot be handled locally, and should propagate up the stack (e.g., file not found, database down)
- Utilizați coduri de eroare pentru operațiuni de înaltă frecvență în care eșecul este normal și de așteptat (de exemplu, ratarea căutării în cache, I/O care ar bloca I/O neblocante)
- Folosiți
std::expectedpentru codul nou în care doriți o gestionare explicită a erorilor cu tipuri bogate de erori și compoziție monadică - Utilizați excepții în constructori — acestea sunt singura modalitate de a semnala eșecul înainte ca obiectul să existe
- Nu introduceți niciodată destructori - înregistrați eroarea și suprimați-o
- Excepțiile, codurile de eroare și
std::expectedsunt instrumente complementare - nu filozofii concurente - Constructorii trebuie să folosească excepții pentru a semnala eșecul; codurile de eroare nu sunt o opțiune
- Destructorii nu trebuie să arunce niciodată - sunt implicit
noexceptde la C++11 - Excepțiile nu depășesc granițele firelor de execuție — utilizați
std::futuresaustd::exception_ptrpentru a le transfera std::error_codeeste ideal pentru erori la nivel de sistem în care doriți un API care nu se aruncă
Test — Verifică-ți cunoștințele
(15 XP)1. De ce sunt excepțiile mecanismul preferat de gestionare a erorilor în constructori?
2. Ce se întâmplă dacă o excepție este aruncată într-un destructor în timpul derulării stivei de la o altă excepție?
3. Cum pot fi transferate excepțiile între fire?