Eccezioni in C++

di il
8 risposte

Eccezioni in C++

Chi programma in Java o C# conosce la potenza dei costrutti try/catch/finally.
Sopprattutto dell'utilizzo del finally, dove e' possibile eseguire eventuali operazioni di cleanup sia che il blocco try sia terminato normalmente, sia con eccezione.

Le altre due funzionalita' estremamente utili e potenti nella gestione delle eccezioni sono:

1) la possibilita' di ricuperare il callstack del punto in cui un'eccezione e' stata generata
2) la possibiliita' di includere all'interno di un'eccezione, un'eventuale altra eccezione

Attenzione, non chiedete 'ma serve?' Se vi ponete questa domanda, e' come se vi chiedeste 'ma l'aria serve?'

In C++ non esiste il finally (a meno di non usare le estensioni di Micro$oft, che comunque sono ancora limitate). E non c'e' un meccanismo standard per includere in'eccezione in un'altra.

Nota: In Java e C# e' estremamente piu' semplice implementare l'inclusione delle eccezioni perche' nella maggior parte dei case gli oggetti sono gestiti mediante reference e non per value.

In C++ il finally generalmente viene implementato mediante il pattern Resource Acquisition Is Initialization: cioe' si utilizza un oggetto di servizio in cui:

1) nel costruttore viene allocata la risorsa
2) nel distruttore viene rilasciata.

Ora, quando si esce da un blocco "{ ... }", il C++ assicura la chiamata dei distruttori degli oggetti ivi allocati. E questo anche se si esce per eccezione.

Quindi, come simulare il try/catch/finally?

Una possibilita' e' questa:

/* 
 * File:   trycatch.hpp
 * Author: ...
 *
 * Created on April 24, 2013, 10:45 AM
 */

#ifndef TRYCATCH_HPP
#define TRYCATCH_HPP

#include <exception>
#include <string>
#include <vector>

/**
 *      _try { ... } 
 *      _end_try
 * 
 *      _try { ... } 
 *      _catch(e) { ... } 
 *      _catch_all { ... } 
 *      _end_catch
 * 
 *      _try { ... } 
 *      _finally { ... } 
 *      _end_finally
 * 
 * NON supportato:
 * 
 *      _try { ... }
 *      _catch(e) { ... }
 *      _catch_all{ ... }
 *      _finally { ... }
 *      _end_finally
 */

namespace hls 
{
    class try_t {
        /**
         * Usato per mantenere traccia del top dello stach dei try/catch
         * 
         * Nota: DEVE essere un threadlocal perche' le relazioni tra i thread
         * generano un tree.
         */
        static          try_t  try_end;
        static __thread try_t* try_top;
        
        /// se e' stato istanziato in una try
        bool        _is_try;
        
        /// se l'eccezione E' stata istanziata per forzare la 'finalize'
        bool        _is_finally;
        
        /// link al try precendete
        try_t*      _prev;
        
        /// nome del file/linea/nome della funzione
        const char* _filename;
        int         _line;
        const char* _function;
        
        /// link al precedente try
        try_t* prev()  const { return _prev; }
        
        /// la classe 'exception_t' puo' accedere alle informazioni di
        /// questa classe
        friend class exception_t;
        
    public:
        /// costruttore usato nella macro '_try'
        try_t(const char* fn, int l, const char* fu);
        
        try_t();
        try_t(const try_t& t);
       ~try_t();
       
        try_t& operator =(const try_t& t);
        
        const char* filename() const { return _filename; }
        int         line()     const { return _line; }
        const char* function() const { return _function; }

        void finalize();
        bool is_finally()      const { return _is_finally; }
    };

    class exception_t : std::exception {
        std::vector<try_t>  _callstack;
        std::string _what;
        
        void fill_stack();
    public:
        exception_t();
        
        exception_t(const char* msg);
        exception_t(const std::string& msg);
        
        virtual ~exception_t() throw ();
        
        virtual const char* what() const throw() { return _what.c_str(); }
        
        std::vector<try_t>& callstack() const { return (std::vector<try_t>&)_callstack; }
        
        void set(const char* fn, int l, const char* fu);
    };
}



#define _try            { hls::try_t try_info(__FILE__,__LINE__,__FUNCTION__); try { 
#define _end_try        } catch(...) { }}

#define _catch(e)       } catch(e)   {
#define _catch_all      } catch(...) {
#define _end_catch      }}

#define _finally        try_info.finalize(); } catch(...) {
#define _end_finally    if (!try_info.is_finally()) throw; }}

#endif	/* TRYCATCH_HPP */

/* 
 * File:   trycatch.cpp
 * Author: ...
 *
 * Created on April 24, 2013, 10:48 AM
 */

#include "trycatch.hpp"

using namespace hls;

__thread try_t* try_t::try_top = 0;

// --------------------------------------------------------------------------
// try_t
// --------------------------------------------------------------------------

try_t::try_t(const char* fn, int l, const char* fu)
    : _filename(fn), _line(l), _function(fu)
{ 
    _is_try = true;
    _prev = try_top;
    try_top = this;
}

try_t::try_t() 
    : _filename(0), _line(0), _function(0)
{
    _is_try = false;
    _prev = 0;
}

try_t::try_t(const try_t& t) 
    : _filename(t._filename), _line(t._line), _function(t._function)
{
    _is_try = false;
    _prev = 0;
}

try_t::~try_t() 
{ 
    if (_is_try)
        try_top = _prev;
}

try_t& try_t::operator =(const try_t& t) {
    _filename = t.filename();
    _line     = t.line();
    _function = t.function();
    _prev = 0;
    return *this;
}


void try_t::finalize() {
    _is_finally = true;
    throw this;
}

// --------------------------------------------------------------------------
// exception_t
// --------------------------------------------------------------------------

exception_t::exception_t() {
    fill_stack();
}

exception_t::exception_t(const char* msg) {
    _what = msg;
    fill_stack();
}

exception_t::exception_t(const std::string& msg) {
    _what = msg;
    fill_stack();
}

exception_t::~exception_t() throw () {
    _callstack.clear();
}

// --------------------------------------------------------------------------

void exception_t::fill_stack() {
    try_t here;
    _callstack.push_back(here);

    for(try_t* top = try_t::try_top; top != 0; top=top->prev())
        _callstack.push_back(*top);
}

void exception_t::set(const char* fn, int l, const char* fu) {
    try_t& here = _callstack[0];
    here._filename = fn;
    here._line     = l;
    here._function = fu;
}

A partire da questa libreria, si puo' scrivere:

/* 
 * File:   main.cpp
 * Author: ...
 *
 * Created on April 21, 2013, 9:32 AM
 */

#include <cstdlib>
#include <iostream>
#include <boost/format.hpp>
#include <hls/trycatch.hpp>
#include <exception>


using namespace std;
using namespace boost;


/*
 * 
 */
int main(int argc, char** argv) {
    
    _try
    {
        hls::exception_t e("I am dead!");

        _try
        {
            e.set(__FILE__,__LINE__,__FUNCTION__);

            std::cout << boost::format("%1%\n") % "Hello Cruel World";

            //throw e;
        }
        _finally
        {
            std::cout << boost::format("%1%\n") % "Finally";
        }
        _end_finally
        
        throw e;
        
        //std::cout << boost::format("%1%\n") % "Hello Cruel World";
    }
    _catch(hls::exception_t& e) {
        std::cout << boost::format("-) %1%:%2% %3%\n") % try_info.filename() % try_info.line() % try_info.function();
        
        std::vector<hls::try_t>& cs = e.callstack();
        
        std::cout << "Callstack:\n";
        for(std::vector<hls::try_t>::iterator it=cs.begin(); it != cs.end(); ++it) {
            std::cout << boost::format("-) %1%:%2% %3%\n") % it->filename() % it->line() % it->function();
        }
    }
    _catch(std::exception& e) {
        std::cout << boost::format("a) %1%:%2% %3%\n") % try_info.filename() % try_info.line() % try_info.function();
    }
    _catch_all {
        std::cout << boost::format("b) %1%:%2% %3%\n") % try_info.filename() % try_info.line() % try_info.function();
    }
    _end_catch
    
    return 0;
}
(nota: al posto della libreria boost, si puo' utilizzare l'operatore "<<" con "std::cout")

Al momento, la libreria supporta solo le seguenti sintassi:


_try { ... } _end_try

intercetta tutte le eccezione,e non le propaga

_try { ... } _catch(e) { ... } _catch_all { ... } _end_catch
intercetta l'eccezione indicata (o tutte le eccezioni)

try { ... } _finally { ... } _end_finally
la sezione finally viene eseguita sia in presenza di eccezioni, che in assenza. Se e' stata generata un'eccezione, questa viene propagata.


Ed ora la domandona: si puo' fare di meglio?

8 Risposte

  • Re: Eccezioni in C++

    Un articolo sulla questione:
    http://www.eptacom.net/pubblicazioni/pub_it/iso_12.htm
  • Re: Eccezioni in C++

    Lo conosco. E' abbastanza datato.

    In C++11 non hanno voluto introdurre la keyword 'finally' perche' osteggiata da Stroustrup.
    Ora, e' vero che ci sono pattern alternativi, ma nessuno e' diretto come questo.

    Non conosco implementazione/template/macro/librerie di terze parti in grado di fare meglio di quanto e' stato fatto qui.
  • Re: Eccezioni in C++

    Si, l'articolo è datato, ma non per questo meno efficace nello spiegare perché in C++ non c'è il costrutto finally e perché non sia necessario. Java e C# non possono utilizzare il paradigma RAII per la gestione degli oggetti, C++ si. E' tutta qui la differenza.
    In C++11 non hanno voluto introdurre la keyword 'finally' perche' osteggiata da Stroustrup.
    Il che denota una precisa scelta progettuale: se l'intera standard library è stata costruita intorno al paradigma RAII, l'introduzione di un finally stravolgerebbe l'intero impianto della libreria stessa. E comunque non solo il C++11, ma nemmeno in C++98, C++03 e C++14 (prossima minor revision dello standard) il comitato ISO ha ritenuto necessario l'introduzione di un finally, dato che il paradigma RAII si è rivelato ben più efficace e pulito nel gestire il clean up degli oggetti in caso di eccezioni.
    Non conosco implementazione/template/macro/librerie di terze parti in grado di fare meglio di quanto e' stato fatto qui.
    Beh, non mi è mai capitato vedere implementazioni di librerie che cercano di riprodurre un finally "artificioso". E in quelle che ho fatto io non ho mai sentito il bisogno di un costrutto finally in C++. Ma non escludo che in futuro la cosa si faccia sentire.
  • Re: Eccezioni in C++

    Attenzione: il C++ non e' solo sintassi o STL.

    Ma e' anche l'insieme di librerie standard (non le STL, evidentemente, le altre) e aggiuntive al contorno (non quelle create in proprio, pensate direttamente ad oggetti) che vengono utilizzate nella realizzazione della soluzione software del momento.

    E quelle, nella maggior parte dei casi, sono scritte secondo la filosofia C.

    Immagino, a questo punto che anche tu farai quello che fa ogni buon sviluppatore C++: si crea la classica libreria wrapper che converte la libreria C in una che segue la filosofia C++, con oggetti, gestione delle eccezioni, ...

    Detto in parole povere: che rottura di cabasisi .

    (e qui: beate boost: sante per sempre )

    Ma per due motivi:

    1) per purista, che deve farsi ogni volta questo wrapper (per carita', e' sempre una cosa buona avere uno strato intermedio tra l'applicazione finale e la libreria di terze parti)

    2) per il principiante, che non ha una gran dimestichezza (per essere generosi) con design patterns, e tecniche consolidate per risolvere specifiche situazioni di programmazione.

    L'utilizzo di una keyword come la finally permeterebbe di scrivere del codice piu' semplice, mettendo a disposizione un meccanismo standard (rappresentato da questa unica keyword) per eseguire le neccessarie operazioni di cleanup in tutte quelle sitazioni in cui non e' gia' applicabile il pattern RAII (e ce ne sono!)

    Infine, la sua introduzione non vuol dire che le STL devono essere stravolte o modificate in qualsiasi modo. Non ci sono effetti collaterali.

    Il fatto e' che se il C++ fosse accompagnato da sole librerie scritte in C++, sarei perfettamente daccordo con te, ma al momento cosi' non e'. E prevedo che non lo sara' mai.
  • Re: Eccezioni in C++

    Aggiungo ancora una nota, che forse ti e' sfuggita:

    le quattro righe di codice che ho proposto non si limitano a implementare (in modo approssimativo, perfettamente daccordo) il costrutto finally, ma fanno una cosa moolto piu' importante:

    mettono a disposizione un meccanismo per l'identificazione del callstack in cui un'eccezione si e' verificata.

    Certo, non e' perfetto, ma e' meglio che niente .

    Personalmente l'ho trovato fondamentale in diverse situazioni di malfunzionamento. Non mi risulta esistano librerie di terze parti in grado di fare l'analisi dello stack e ricostruire la sequenza di chiamate.

    Ed e' anche ovvio.

    C/C++ non si portano dietro le informazioni sul nome delle funzioni o delle variabili.
  • Re: Eccezioni in C++

    migliorabile ha scritto:


    Attenzione: il C++ non e' solo sintassi o STL.
    Infatti. E' soprattutto un modo di pensare diverso da quanto si fa in C o in Java.
    Tra l'altro in diversi compilatori C++ le eccezioni si possono disattivare, per cui tutto il meccanismo dietro non funzionerebbe (i distruttori invece si).
    Detto in parole povere: che rottura di cabasisi .
    Prova a creare un wrapper per un oggetto COM e ne riparliamo.
    1) per purista, che deve farsi ogni volta questo wrapper (per carita', e' sempre una cosa buona avere uno strato intermedio tra l'applicazione finale e la libreria di terze parti)
    Per purista chi intendi? Qualcuno che preferisce gestirsi a mano tutte le risorse e ogni tanto ne dimentica una per strada?
    2) per il principiante, che non ha una gran dimestichezza (per essere generosi) con design patterns, e tecniche consolidate per risolvere specifiche situazioni di programmazione.
    Al principiante si devono insegnare i paradigmi di programmazione che rendono solido un linguaggio: RAII in primis; non aggirare il problema introducendo soluzioni non native.
    Il finally in Java è indispensabile perché non è possibile creare oggetti sullo stack e il distruttore non è deterministisco. Non è che sia una "feature" del linguaggio: è una toppa.
    Già i principianti dimenticano di liberare la memoria con una delete o free quando serve, figurarsi se oltre a questo devono ricordarsi di usare il finally per liberare la memoria.
    Per la cronaca, il paradigma RAII è stata la prima cosa che ho imparato quando ero principiante.
    L'utilizzo di una keyword come la finally permeterebbe di scrivere del codice piu' semplice,
    Facendo qualche ricerca appare il contrario. Domande sul perché in C++ non è stato messo il finally ce ne sono e tutte le risposte indicano di usare il paradigma RAII perché superiore.
    http://www.informit.com/guides/content.aspx?g=cplusplus&seqNum=189
    http://stackoverflow.com/questions/161177/does-c-support-finally-blocks-and-whats-this-raii-i-keep-hearing-about
    Qualcuno ha anche creato questo:
    http://www.codeproject.com/Tips/476970/finally-clause-in-Cplusplus
    che, guarda caso, utilizza il paradigma RAII per gestire la risorsa.

    mettendo a disposizione un meccanismo standard (rappresentato da questa unica keyword) per eseguire le neccessarie operazioni di cleanup in tutte quelle sitazioni in cui non e' gia' applicabile il pattern RAII (e ce ne sono!)
    Non lo escludo, ma al momento non me ne vengono in mente. Esempi?
    Infine, la sua introduzione non vuol dire che le STL devono essere stravolte o modificate in qualsiasi modo. Non ci sono effetti collaterali.
    Non ci metterei la mano sul fuoco. A mio avviso, però, l'introduzione del finally è una regressione nel modo di scrivere codice C++ moderno.
    Il fatto e' che se il C++ fosse accompagnato da sole librerie scritte in C++, sarei perfettamente daccordo con te, ma al momento cosi' non e'. E prevedo che non lo sara' mai.
    Non vedo cosa centri con il clean up delle risorse.
    mettono a disposizione un meccanismo per l'identificazione del callstack in cui un'eccezione si e' verificata.
    Per me è sufficente sapere il punto dove si è verificata l'eccezione, non avere tutto il callstack delle chiamate. Ognuno però ha il suo modo di vedere le cose, pertanto se ti trovi meglio così non può farmi che piacere.
  • Re: Eccezioni in C++

    Mi hai ricordato una battuta che ho letto non tanto tempo fa: se uno ha in un mano un martello, tutti i problemi sono dei chiodi

    Affermare che il finally e' una toppa e' un po' riduttivo, non ti pare?
    Non esiste solo il C++. E non esiste solo la gestione della memoria esplicita (per gli altri: che non usa il GC).

    Utilizzare paradigmi di programmazione implementati in qualche altro linguaggio non e' una cosa da scartare a priori solo perche' hai a disposizione un sistema piu' potente per fare la stessa cosa.

    Per andare dal punto A al punto B non e' detto che ti serva il SUV (RAII): ti puo' bastare la bicicletta (finally).

    Piccola nota: evitiamo le polemiche (il sarcasmo) o le crociate stile Windows/Linux.
    Un buon programmatore deve (non dovrebbe) conoscere diversi paradigmi/linguaggi di programmazione e sfruttare le idee buone di ogn'uno di essi, indipendentemente da quale uso in quel momento, che limitarmi ad uno solo (C++) e considerare inutile tutto il resto.

    Basta notare, ad esempio, che in tutti i tre linguaggi OO principali attuali (C++. Java. C#) stanno introduccendo concetti di programmazione funzionale. Ma la FP non e' un'invenzione di questi anni. Esiste dal '56, su concetti del '30 (il lambda calcolo). Il fatto di essere inseriti in un framework coerente ed integrato con il linguaggio ora, non vuol dire che prima non si potevano utilizzare.
  • Re: Eccezioni in C++

    migliorabile ha scritto:


    Mi hai ricordato una battuta che ho letto non tanto tempo fa: se uno ha in un mano un martello, tutti i problemi sono dei chiodi
    Almeno finché in mezzo non ci finisce un dito
    Affermare che il finally e' una toppa e' un po' riduttivo, non ti pare?
    Dipende dai punti di vista. Dal punto di vista C++ il finally è una toppa perché non appicabile il paradigma RAII: dal punto di vista di Java l'ereditarietà a rombo permessa dal C++ è puro orrore, la mancanza di un GC è puro orrore, il fatto che sia "bicefalo" (parte OOP, parte funzionale) è puro orrore.
    Non esiste solo il C++. E non esiste solo la gestione della memoria esplicita (per gli altri: che non usa il GC).
    Ovviamente no, ma il topic è sul C++ non su altri linguaggi. Ogni linguaggio implementa le sue strategie, i suoi paradigmi; ma se programmo in C++ devo "pensare" in C++, se programmo in Java devo "pensare" in Java e così via.
    Utilizzare paradigmi di programmazione implementati in qualche altro linguaggio non e' una cosa da scartare a priori solo perche' hai a disposizione un sistema piu' potente per fare la stessa cosa.
    Ubi major, minor cessat. Perché non devo usare una soluzione potente e nativa e accontentarmi invece di una soluzione meno potente e raffazzonata? Perché devo farmi carico di dover gestire manualmente tutte le risorse (facendo attenzione a non farmene sfuggire una), quando posso fare in modo che sia un distruttore deterministico a farlo?
    E potrei vedere la questione anche come: perché Java e C# non implementano il paradigma RAII, quando in C++ si è dimostrato molto efficace?
    Per andare dal punto A al punto B non e' detto che ti serva il SUV (RAII): ti puo' bastare la bicicletta (finally).
    Dipende dai punti. Se A è la base di una montagna e B la cima è più comodo andarci in SUV.
    Il fatto è che tutti i vari esempi portati riguardano poche righe di codice e sono solo di esempio.
    Quando le righe di codice superano il centinaio per funzione, diventa complicato tenere sotto controllo il tutto e tenere traccia di tutte le risorse usate.
    Piccola nota: evitiamo le polemiche (il sarcasmo) o le crociate stile Windows/Linux.
    Mai interessato alle crociate o alle guerre di religione, men che meno sui linguaggi di programmazione. Ma se due linguaggi nascono su basi diverse, introdurre in uno cose dell'altro non è detto che produca buoni frutti.
    Un buon programmatore deve (non dovrebbe) conoscere diversi paradigmi/linguaggi di programmazione e sfruttare le idee buone di ogn'uno di essi, indipendentemente da quale uso in quel momento, che limitarmi ad uno solo (C++) e considerare inutile tutto il resto.
    Concordo.
    Basta notare, ad esempio, che in tutti i tre linguaggi OO principali attuali (C++. Java. C#) stanno introduccendo concetti di programmazione funzionale. Ma la FP non e' un'invenzione di questi anni. Esiste dal '56, su concetti del '30 (il lambda calcolo). Il fatto di essere inseriti in un framework coerente ed integrato con il linguaggio ora, non vuol dire che prima non si potevano utilizzare.
    Se sono esigenze sentite ben vengano, ma l'introduzione del finally in C++ non è una di queste (per ora almeno). Come ho già scritto, cercando con google "c++ finally" non ho trovato una sola voce che difenda il finally in C++. Tutti dicono: usa RAII. Vorrà pur dire qualcosa, credo.
Devi accedere o registrarti per scrivere nel forum
8 risposte