API C in C ++ con RAII, due alternative per implementare la gestione degli errori (Eccezioni)

1

Ho un'API scritta in C, che produce un risultato restituendo un puntatore alla memoria allocata.
Per usarlo con C ++ (C ++ 11) ho avvolto le chiamate di funzione negli oggetti, che mantengono il risultato in std::shared_ptr . Fin qui tutto bene.

Tuttavia, la libreria C offre funzioni due per ogni operazione. Uno produce probabilmente un errore, l'altro mai. Chiamiamoli su some_pod * do_it_with_error(Parameter ..., Error **)
e
some_pod * do_it_without_error(Parameter ...)

Posso passare l'indirizzo di un puntatore Error * alla prima funzione e se c'è un errore non sarà più NULL in seguito.

Per risolvere ciò ho pensato a due diverse implementazioni.
Innanzitutto, potrei SFINAE per scegliere tra le funzioni with_error e without_error , in questo modo:

template<typename NoThrow = void>
struct do_it {
  public:
    void operator()(...)
    {
      Error * error = NULL;
      m_result = std::shared_ptr<some_pod>(do_it_with_error(..., &error));
      if (error) {
        throw(MyExceptionWithError(error));
      }
    }
  private:
    std::shared_ptr<some_pod> m_result;
};

template<>
class do_it<std::nothrow_t> {
  public:
    void operator()(...) noexcept
    {
      if (! m_result) {
        m_result = std::shared_ptr<some_pod>(do_it_without_error(...));
      }
    }
  private:
    std::shared_ptr<some_pod> m_result;
};

Utilizzo:

do_it<> di_error;
try {
  di_error(...); // might throw
} catch (...) {}

do_it<std::nothrow_t> di_no_error;
di_no_error(...); // won't throw for sure

Tuttavia, poiché il risultato è incluso in std::shared_ptr , ci sarà anche un metodo get() :

const std::shared_ptr<some_pod> & get(void) { return m_result; }

Per il controllo degli errori, potrei semplicemente implementare un altro metodo check .
L'implementazione predefinita sarebbe ora simile a questa:

struct do_it {
  public:
    // will never throw
    const std::shared_ptr<some_pod> &
    get(void) noexcept
    {
      if (! m_result) {
        m_result = std::shared_ptr<some_pod>(do_it_without_error(...));
      }
      return m_result;
    }

    // might throw
    do_it &
    check(void)
    {
      if (! m_result) {
        Error * error = NULL;
        m_result = std::shared_ptr<some_pod>(do_it_with_error(..., &error));
        if (error) {
          throw ...;
        }
      }
      return *this;
    }

  private:
    std::shared_ptr<some_pod> m_result;
};

Utilizzo:

do_it di_error;
try {
  auto result = di_error.check().get();
} catch (...) {}
do_it di_no_error;
di_no_error.get();

Quindi, entrambe le implementazioni sembrano essere ugualmente buone (o cattive) per me.
Come potrei decidere quale usare.
Quali sono i pro e i contro?

    
posta jrk 05.02.2014 - 11:11
fonte

3 risposte

1

Troppo complicato - avvolgeteli entrambi come se potessero lanciare un errore, che il 2 ° fn non getterà mai solo significa che il compilatore non dovrà mai chiamare le routine di gestione degli errori (e potrebbe anche ottimizzarlo del tutto) - in entrambi i casi il codice funzionerà altrettanto bene grazie al design del C ++.

Il tuo codice sarà anche molto più facile da capire, anche con questa gestione degli errori ridondanti (che un giorno potrebbe essere utilizzato per altri errori imprevisti)

    
risposta data 05.02.2014 - 12:08
fonte
1

In C gli oggetti di errore sono piuttosto difficili da gestire, quindi c'è l'altro sovraccarico che memorizza gli errori da qualche parte. Ma nelle eccezioni C ++ rende tutto molto più facile da gestire. Quindi vorrei solo racchiudere la variante che riporta errori e convertire gli errori in eccezioni e ignorare che esiste anche l'altra API.

    
risposta data 05.02.2014 - 13:23
fonte
0

Per ogni operazione, hai tre soggetti distinti su cui occuparti, ad esempio, del carico di lavoro, del risultato e della gestione degli errori. Vediamoli tutti in isolamento:

  1. Carico di lavoro : non dovrebbe essere duplicato. Chiamare sempre la funzione C producendo probabilmente informazioni sull'errore, poiché è la più completa di ciascuna coppia.

  2. Risultato : i POD possono essere mantenuti come sono nell'API C e in attesa / recuperati tramite oggetti shared_ptr , ma questa è solo una delle molte opzioni possibili -se, certamente, abbastanza semplice. Forse alcuni di questi POD valgono la pena di essere inclusi in, o convertiti in, classi copy-constructible / move-constructible e direttamente restituiti dalla chiamata operator() .

  3. Gestione degli errori : il modo in cui gli oggetti della funzione C ++ fanno (o non) comunicano le informazioni relative agli errori è secondario e ortogonale al carico di lavoro principale. Idealmente, si desidera essere in grado di modificare il meccanismo di gestione degli errori in base al contesto, senza influire sul carico di lavoro stesso. Quindi consideralo come una dipendenza che si inietta sull'oggetto funzione a proprio piacimento, sia in fase di costruzione che in fase di chiamata. In questo modo, è possibile aggiungere ulteriori meccanismi di gestione degli errori in futuro, diversi da quelli attuali di mancata pubblicazione e di eccezione (cosa succede se si termina volendo notificare errori con i messaggi della console, ad esempio?).

Finirò fornendo una semplice implementazione che potrebbe servire come esempio:

class DoSomeStuff
{
public:
    class Result
    {
    public:
        Result() : _result() {}
        Result(const Result &rhs) : _result(rhs) {}
       ~Result() {}

        Result &operator=(const Result &rhs)
        {
            if(&rhs != this)
                _result = rhs._result;

            return *this;
        }

        int GetSomeData() const { return _result->_some_int_value; }

    private:
        shared_ptr<some_pod> _result;
    };

    struct IErrorHandler
    {
        virtual void NotifyError(Error &) = 0;
    };

    Result operator()(/*params*/, IErrorHandler &errorHandler)
    {
        Error *error = { nullptr };

        shared_ptr<some_pod_type> result = { do_some_stuff_with_error(/*params*/, &error) };

        if(error != nullptr)
            errorHandler.NotifyError(*error);

        return Result(result);
    }
};

Notare che Result e IErrorHandler non devono essere inclusi nella classe della funzione; Ho fatto in questo modo perché sono entrambe le dipendenze, ma possono essere estratte per evitare la duplicazione tra diverse classi di funzioni.

    
risposta data 06.02.2014 - 00:34
fonte

Leggi altre domande sui tag