1. Introduzione
Quando si impara a programmare può sembrare che gli errori siano di due tipi: «ho dimenticato il punto e virgola» e «tutto il resto». Ma man mano che ci si avvicina al mondo reale (input, file, permessi, rete, SO), molti errori diventano prevedibili: un file può non esistere, una directory può essere inaccessibile, un nome può essere già occupato. std::error_code è un modo per parlare di questi fallimenti con calma, come di dati, senza panico e senza eccezioni.
In parole semplici: std::error_code è una «ricevuta d'errore». Una ricevuta vuota significa successo. Non vuota — contiene un «codice» e una «categoria», così da capire cosa è andato storto e prendere una decisione.
È importante che nella libreria standard ci siano operazioni che sanno non lanciare eccezioni ma invece scrivere informazioni sull'errore in std::error_code& (per esempio, overload in std::filesystem — esamineremo il modulo filesystem più avanti, ma sapere che esistono queste overload è già utile).
Modello mentale: «errore come valore», ma standardizzato
Se avete letto codice da principianti sapete lo stile classico: «restituisco -1, poi vedrete». Il problema è che -1 potrebbe essere un valore legittimo, e il «poi vedrete» spesso diventa «nessuno ha capito».
std::error_code propone un modello standard:
- il successo è rappresentato da uno std::error_code vuoto (valore di default);
- l'errore è rappresentato da uno std::error_code non vuoto (controllabile con if (ec)).
Ed è perfettamente coerente con la nostra filosofia del giorno: «l'errore è dato».
Un piccolo schema di come normalmente appare questo contratto:
flowchart TD
A["La funzione esegue un'operazione"] --> B{"Successo?"}
B -->|sì| C["Restituisce valore o scrive nel parametro out"]
B -->|no| D["Restituisce std::error_code (non vuoto)"]
D --> E["Il chiamante decide: riprovare, segnalare, terminare"]
Un dettaglio importante: std::error_code è particolarmente adatto dove l'errore è parte del normale controllo di flusso. Per esempio: «prova ad aprire», «se non si apre — non è la fine del mondo, segnala e continua».
2. Interfaccia minima di std::error_code: cosa ricordare
Buone notizie: non serve conoscere std::error_code in ogni dettaglio di metodi e categorie. Basta un piccolo insieme per scrivere codice pulito e leggibile.
Si include così:
#include <system_error>
E lo si usa più spesso così:
- std::error_code ec; — «per ora tutto ok».
- if (ec) — «è comparso un errore».
- ec.message() — messaggio leggibile dall'umano (per mostrare all'utente).
- ec.value() — codice numerico (di solito per diagnostica/log, non per logica «magica»).
- confronto con altri std::error_code (per esempio quelli ottenuti da std::errc).
Ecco il più piccolo esempio per «sentirlo con le mani»:
#include <iostream>
#include <system_error>
int main() {
std::error_code ec; // vuoto = successo
std::cout << "ok? " << std::boolalpha << !ec << '\n'; // ok? true
// Simuliamo un errore: creiamo un error_code da una ragione standard
ec = std::make_error_code(std::errc::invalid_argument);
if (ec) {
std::cout << "error: " << ec.message() << '\n'; // il testo dipende dal SO/locale
std::cout << "value: " << ec.value() << '\n';
}
}
Nota importante: il testo di message() può variare tra i sistemi. Quindi message() è comodo per la stampa, ma non è consigliabile usarlo come base della logica («se message == "..." allora…»). La logica è meglio basarla sul confronto di codici/causa.
4. std::errc: cause di errore portabili
Per evitare di confrontare ec.value() con numeri misteriosi come 2, 13 o 12345 (e poi cercare di notte cosa fossero), lo standard fornisce l'enum std::errc. È un insieme di cause tipiche come «file non trovato», «permesso negato», «argomento non valido».
Il bello di std::errc è che:
- scrivi codice più leggibile per significato;
- la libreria standard può convertire un errc in uno error_code.
Lo strumento chiave è std::make_error_code.
Esempio di controllo «è un errore di argomento non valido?»:
#include <system_error>
bool is_invalid_input(const std::error_code& ec) {
return ec == std::make_error_code(std::errc::invalid_argument);
}
Qui la logica è più robusta rispetto a «confrontare ec.value() con 22» (potresti avere un altro SO, altra libreria, altra piattaforma).
5. Stile tipico senza eccezioni: parametro out + std::error_code
Ora costruiamo lo «stile più standard» per una funzione che non vuole lanciare eccezioni ma vuole segnalare l'errore. Di solito la funzione restituisce uno std::error_code e mette il risultato nella variabile passata per riferimento.
Sì, è un po' «old-school», ma ha il grande vantaggio di integrarsi bene con molte API della standard library dove l'errore è un esito previsto.
Aggiungiamo al progetto di esempio un parser di numeri curato
Immaginiamo di sviluppare un'applicazione da riga di comando (chiamiamola convenzionalmente TaskCLI) che riceve comandi dall'utente. Nei comandi spesso ci sono numeri: id del task, priorità, limite. Ci serve un parser «senza eccezioni» e senza optional, perché a volte è utile conoscere la ragione dell'errore almeno a livello «input non valido».
Scriviamo parse_uint_ec: se ha successo — scrive in out, altrimenti — restituisce uno error_code.
#include <string_view>
#include <system_error>
std::error_code parse_uint_ec(std::string_view text, unsigned& out) {
if (text.empty()) {
return std::make_error_code(std::errc::invalid_argument);
}
unsigned value = 0;
for (char c : text) {
if (c < '0' || c > '9') {
return std::make_error_code(std::errc::invalid_argument);
}
value = value * 10u + static_cast<unsigned>(c - '0');
}
out = value;
return {}; // error_code vuoto = successo
}
È importante che return {} significhi «tutto ok». Non true, non 0, non -1. Semplicemente uno std::error_code vuoto.
Uso vicino alla chiamata
Ora chiamiamo:
#include <iostream>
#include <string>
#include <system_error>
std::error_code parse_uint_ec(std::string_view text, unsigned& out);
int main() {
std::string s;
std::cin >> s;
unsigned id = 0;
std::error_code ec = parse_uint_ec(s, id);
if (ec) {
std::cerr << "Invalid id: " << ec.message() << '\n';
return 1;
}
std::cout << "Parsed id = " << id << '\n';
}
Nota lo stile corretto: controlliamo ec subito, vicino alla chiamata. È come controllare se hai chiuso la porta uscendo di casa, non tre settimane dopo dalla spiaggia.
6. Come applicare std::error_code in un progetto: contesto, stampa, esempio
Dove si incontra std::error_code nella standard library
È importante capire il contesto: std::error_code non è «un nostro giocattolo didattico». È uno dei modi ufficiali con cui la standard library restituisce informazioni d'errore senza eccezioni.
Spesso appare come «overload con parametro aggiuntivo std::error_code&». Per esempio, nelle operazioni sui file (il modulo su std::filesystem verrà dopo) potresti incontrare funzioni che prendono un error_code e invece di lanciare un'eccezione ci scrivono l'errore.
Per vedere l'idea (senza entrare nel filesystem), pensa a questo pattern:
// Forma pseudo: "fai, e scrivi l'errore in ec"
operation(args..., std::error_code& ec);
Dove ec dopo la chiamata:
- vuoto → successo;
- non vuoto → errore, e tu decidi cosa fare.
E qui std::errc è ancora utile: puoi confrontare la «causa» con valori portabili invece di basare la logica sul testo dell'errore.
Coerenza con il nostro modello d'errori: come evitare uno «zoo di contratti»
È facile creare uno «zoo di contratti» dove una funzione restituisce optional, un'altra variant, una terza error_code, e una quarta stampa su cout e prosegue tranquillamente.
Per evitarlo mantieni un'idea semplice: std::error_code non compete con expected, è uno strumento per situazioni specifiche.
Ecco una piccola tabella:
| Strumento | Cosa esprime | Quando è più adatto |
|---|---|---|
|
«valore presente/assente» | «non trovato» come ramo normale, senza motivo |
|
«successo o errore con motivo» | quando vuoi il tuo modello ricco ErrorKind + message + context |
|
«più esiti significativi» | quando ci sono più di due esiti e sono tutti importanti |
|
«codice di errore + categoria» | quando sei nella «zona di sistema»: I/O, standard library/SO, fallimenti previsti |
Quindi nel nostro TaskCLI la logica degli errori di business può vivere in Error (come nelle lezioni precedenti), mentre dove comunichiamo con il «mondo esterno» (stream, file, environment) è spesso più naturale usare/ritornare std::error_code e poi convertirlo nel nostro formato.
Come stampare bene uno error_code
I principianti spesso fanno così: «se errore — stampo tutto ovunque, in tre posti». Poi ottengono tre messaggi identici e uno contraddittorio, e non resta che guardare filosoficamente fuori dalla finestra.
Approccio base «educato»: la stampa deve essere breve e il punto di stampa prevedibile (di solito al bordo del programma: handler CLI del comando, main, livello superiore dello script). Non costruiamo una policy completa, ma una piccola funzione di stampa utile:
#include <iostream>
#include <system_error>
void print_ec(const std::error_code& ec) {
if (!ec) {
std::cout << "OK\n";
return;
}
std::cerr << "ERROR: " << ec.message() << '\n';
}
È importante non esagerare: message() basta per far capire all'utente cosa è successo. value() si può aggiungere per la diagnostica, ma non come base semantica.
Mini-flusso in un'app: parsing del comando delete <id> senza eccezioni
Costruiamo un piccolo pezzo di progetto per avere la sensazione di non essere in isolamento ma di sviluppare un'app reale.
Immaginiamo il comando di delete come una stringa, e che l'id sia già parsato tramite parse_uint_ec. Se il parsing fallisce — segnaliamo. Se riesce — tentiamo la cancellazione (qui la cancellazione è semplificata a uno stub, perché ci interessa la gestione dell'errore di input).
#include <iostream>
#include <string_view>
#include <system_error>
std::error_code parse_uint_ec(std::string_view text, unsigned& out);
std::error_code handle_delete(std::string_view id_text) {
unsigned id = 0;
std::error_code ec = parse_uint_ec(id_text, id);
if (ec) return ec;
// Supponiamo che poi si cerchi il task per id e si elimini.
// Qui mostriamo solo il livello di input.
return {};
}
int main() {
// Esempio: l'utente ha inserito "abc" invece di un numero
std::error_code ec = handle_delete("abc");
if (ec) {
std::cerr << "delete failed: " << ec.message() << '\n';
return 1;
}
std::cout << "deleted\n";
}
Importante in questo mini-flusso: handle_delete non stampa internamente. Restituisce lo status e la stampa avviene al livello superiore. Questo riduce il rischio di «repliche duplicate» dall'applicazione.
7. Errori tipici nell'uso di std::error_code
Errore #1: confrontare ec.value() con «numeri magici».
Spesso inizia innocuamente con «qui il mio 2 significa “file non trovato”», e finisce che il codice funziona solo su una macchina e solo il martedì. Meglio confrontare con std::errc tramite std::make_error_code, perché esprime il significato ed è meno legato alla piattaforma.
Errore #2: usare ec.message() come base della logica.
message() serve per mostrare testo all'utente, ma dipende da sistema, locale e implementazione. Se inizi a scrivere «se message contiene "denied"», il tuo codice diventerà presto fragile. Per la logica usa i codici/causa; lascia il testo per l'output.
Errore #3: dimenticare di controllare lo error_code e proseguire come se tutto fosse a posto.
std::error_code non ti obbliga a verificarlo (a differenza del compilatore che ti obbliga a mettere il ;). La disciplina è manuale: hai ricevuto ec — controllalo vicino alla chiamata. Più il controllo è lontano, più è probabile che tu abbia perso il contesto.
Errore #4: stampare l'errore e contemporaneamente restituirlo verso l'alto.
Questo quasi garantisce messaggi duplicati man mano che il progetto cresce. Lo strato interno restituisca error_code, lo strato esterno decida cosa stampare e dove. Altrimenti l'applicazione rischia di «agitarsi e ripetere la stessa cosa».
Errore #5: cercare di curare tutto con std::error_code, anche quando serve una ragione ricca.
std::error_code è ottimo per cause di sistema («argomento non valido», «accesso negato»), ma non sostituisce il tuo modello di errori a livello di dominio. Se ti serve distinguere «task non trovato» da «task già completato» — quello è il tuo ErrorKind/variant/expected, non error_code.
GO TO FULL VERSION