std::expected e la gestione degli errori senza eccezioni

std::expected e la gestione degli errori senza eccezioni

Le eccezioni sono uno strumento potente per gestire gli errori, ma non sono sempre la scelta ideale. In alcuni contesti, come i sistemi embedded, il codice ad altissime prestazioni o le situazioni in cui gli errori sono frequenti e previsti, si preferisce un approccio che renda gli errori parte esplicita del valore di ritorno. Lo standard C++23 introduce std::expected, un tipo che rappresenta il risultato di un'operazione che può concludersi con un valore atteso oppure con un errore, senza ricorrere al meccanismo delle eccezioni.

L'idea di fondo

Un oggetto std::expected è parametrizzato su due tipi: quello del valore atteso in caso di successo e quello dell'errore in caso di fallimento. In ogni momento contiene o l'uno o l'altro. Questo costringe chi chiama la funzione a considerare esplicitamente entrambi gli esiti, poiché l'errore non è nascosto in un canale separato come accade con le eccezioni, ma è parte integrante del valore restituito.

#include <expected>
#include <string>

// Restituisce un intero in caso di successo o un messaggio d'errore
std::expected<int, std::string> divide(int numerator, int denominator) {
    if (denominator == 0) {
        // Segnala l'errore senza lanciare eccezioni
        return std::unexpected("Divisione per zero");
    }
    return numerator / denominator;
}

Il valore di successo si restituisce normalmente, mentre l'errore si avvolge nella funzione std::unexpected, che segnala inequivocabilmente l'esito negativo.

Consumare il risultato

Chi riceve un std::expected verifica l'esito con il metodo has_value oppure valutando direttamente l'oggetto in un contesto booleano. Il valore atteso si ottiene con value, mentre l'errore si estrae con error. Questa simmetria rende il codice di gestione esplicito e leggibile.

#include <iostream>

int main() {
    auto result = divide(10, 0);

    if (result) {
        std::cout << "Risultato: " << result.value() << std::endl;
    } else {
        // Accede all'errore contenuto
        std::cout << "Errore: " << result.error() << std::endl;
    }
    return 0;
}

Anche std::expected offre il metodo value_or, che restituisce il valore atteso oppure un valore di riserva quando l'operazione è fallita, utile quando un errore può essere assorbito con un valore predefinito.

Il confronto con std::optional

A prima vista std::expected somiglia a std::optional, ma la differenza è sostanziale. Un optional comunica soltanto la presenza o l'assenza di un valore, senza spiegarne il motivo; un expected, invece, quando fallisce trasporta un'informazione dettagliata sull'errore. Si sceglie optional quando l'assenza non ha bisogno di spiegazioni, expected quando è importante sapere perché l'operazione non è riuscita.

Concatenare operazioni

Uno degli aspetti più eleganti di std::expected è la possibilità di comporre più operazioni che possono fallire, attraverso metodi funzionali come and_then, che prosegue la catena solo in caso di successo, e transform, che trasforma il valore atteso lasciando inalterato l'eventuale errore. Se un passaggio fallisce, l'errore si propaga automaticamente lungo la catena, senza bisogno di controlli intermedi.

#include <expected>
#include <string>

std::expected<int, std::string> parse(const std::string& text);

void process(const std::string& input) {
    // Il calcolo prosegue solo se il parsing riesce
    auto outcome = parse(input)
        .transform([](int value) { return value * 2; });
}

Vantaggi e considerazioni

La gestione degli errori tramite std::expected rende il flusso di controllo esplicito e prevedibile: non ci sono salti nascosti come con le eccezioni, e ogni possibile errore è visibile nella firma della funzione. Questo approccio è particolarmente apprezzato dove le prestazioni sono critiche o dove le eccezioni sono sconsigliate. Non sostituisce del tutto le eccezioni, che restano appropriate per errori davvero eccezionali e rari, ma offre un'alternativa disciplinata per gli errori attesi e frequenti. Con l'arrivo di std::expected, il C++ dispone finalmente di uno strumento standard ed elegante per un modello di gestione degli errori a lungo diffuso solo tramite librerie di terze parti.