Error handling senza eccezioni

di il
3 risposte

Error handling senza eccezioni

Salve a tutti,

SIccome le eccezioni non mi piacciono, le ritengo poco eleganti (ancor di più se si usa noexcept(false)) ed in più è molto facile abusarne, mi sono ispirato a golang (scommetto che non sono il primo ad averlo fatto) ed ho sviluppato un metodo strutturato in questo modo:

#include <iostream>
#include <string>
#include <variant>

class Error {
    const std::string msg;

public:
    Error(const std::string &msg);

    const std::string &getMsg() const;

    friend std::ostream &operator<<(std::ostream &os, const Error &error);
};


template <class T, class E = Error>
class Result {
    std::variant<T, E> value;
    const bool error;

public:
    Result(E err) : error(true), value(err) {};
    Result(T data): error(false), value(data) {};

    operator bool() const{
        return not error;
    }

    T& operator*() {
        if (error) abort();
        return std::get<T>(value);
    }

    T operator*() const {
        if (error) abort();
        return value.data;
    }

    E& getError() {
        if(not error) abort();
        return std::get<E>(value);
    }

    E getError() const {
        if(not error) abort();
        return value.error;
    }

    bool isError() const {
        return error;
    };
};
questo andrebbe usato nel seguente modo:

Result<std::string> test(bool f) {
    if(f)
        return {"ciao"};
    else
        return Error("Error");
};

int main(int argc, char **argv) {
    
    auto e1 = test(false);

    if(e1) {
        std::cout << *e1 << std::endl;
    } else {
        std::cerr << "[Error]:\t" << e1.getError() << std::endl;
    }
}
Ora vorrei calcolare quanto questo mio metodo sia meno efficiente dell'utilizzo classico delle eccezioni:

#include <iostream>
#include <string>
#include <exception>

std::string test(bool f) noexcept(false){
    if(f)
        return "ciao";
    else
        throw std::string("no");
};

int main(int argc, char **argv) {

    try {
        auto e1 = test(false);
    } catch (std::string &e) {
        std::cout << "[Error]:\t" << e << std::endl;
    };
}
Come posso capire questa cosa? Esistono tool già pronti? devo fare test esattamente come farei per un qualsiasi altro software?

3 Risposte

  • Re: Error handling senza eccezioni

    Dai test che ho fatto ( senza mostrare l'output) su 1 milione di iterazioni a me risulta:
    debug: 2214ms
    release 13ms
    per il codice con eccezioni.

    debug: 5391ms
    relase 23ms
    per il tuo sistema.

    In entrambi i casi passando true alla funzione di test.

    Passando false alla funzione di test ottengo (cambiando leggermente la funzione come mostrato sotto):
    debug: 3490ms
    release: 2061ms
    per il codice con eccezioni

    debug: 7558ms
    release: 27 ms
    per il tuo sistema.

    Funzioni modificate in (al netto del timer che ho usato):
    
    std::string test(bool f) noexcept(false) {
    	if (f)
    		return "ciao";
    	else
    		throw std::runtime_error("no");
    };
    
    bla main(bla bla)
    		try {
    			auto e1 = test1(false);
    		}
    		catch (const std::exception &e) {
    //			std::cout << "[Error]:\t" << e << std::endl;
    		};
    
  • Re: Error handling senza eccezioni

    Grazie,

    Potresti dirmi come hai sviluppato i test? cosa mi conviene guardare solo il tempo di esecuzione?
  • Re: Error handling senza eccezioni

    Ho utilizzato uno std::chrono::high_resolution_clock per misurare il tempo trascorso. il main completo sarebbe
    
    int main (etc) {
    	using clock = std::chrono::high_resolution_clock;
    	using duration = clock::duration;
    	using rep = duration::rep;
    	using period = duration::period;
    
    	auto start = clock::now();
    
    
    	for (int i = 0; i < (1000000); i++) {
    		auto e1 = test(false);
    		if (e1) {
    //			std::cout << *e1 << std::endl;
    		}
    		else {
    //			std::cerr << "[Error]:\t" << e1.getError() << std::endl;
    		}
    	}
    
    	auto stop = clock::now();
    	rep r = (stop - start).count();
    	cout << static_cast<rep>(
    		(static_cast<double>( r * period::num) / period::den) * 1000
    	) << endl;
    }
    
    cosa mi conviene guardare solo il tempo di esecuzione?
    Il punto è che il tuo sistema viene costruito sia che ci sia errore sia che non ci sia, mentre l'eccezione è sollevata solo nel caso di errore.
    Ma più che al tempo di esecuzione in se, si dovrebbe guardare il contesto generale. Ci sono situazioni in cui lanciare un'eccezione è superfluo e altre in cui è indispensabile. Ricordo di aver letto nelle faq del C++ che lanciare un'eccezione è l'unico modo sicuro di segnalare che la costruzione di un oggetto è fallita (i costruttori non restituiscono nulla quindi se la costruzione di un oggetto fallisce, segnalare la cosa diventa molto complicato specie in oggetti complessi). Lo stesso Stroustrup ha affermato che ci sono solo due eccezioni indispensabili: la bad_alloc per la memoria e la bad_type (credo si chiami così) per il dynamic_cast; il resto si può maneggiare senza eccezioni.

    Concordo con te sul fatto che spesso si è abusato delle eccezioni. Infatti andrebbero usate solo nelle situazioni di panico (se si sa che un qualcosa può fallire, non ha senso lanciare un'eccezione).

    Qui c'è un articolo sui pro e contro delle eccezioni:
    https://www.codeproject.com/Articles/38449/C-Exceptions-Pros-and-Cons

    P.S.
    Ti suggerisco di dare un'occhiata a std::optional, se per te non importa avere un messaggio d'errore custom.
Devi accedere o registrarti per scrivere nel forum
3 risposte