Domanda

Sto avendo una discussione con un collega che la generazione di eccezioni da costruttori e di pensiero vorrei qualche feedback.

È OK per lanciare eccezioni costruttori, da un punto di vista?

Diciamo che io sono il confezionamento di un POSIX mutex in una classe, sarebbe qualcosa di simile a questo:

class Mutex {
public:
  Mutex() {
    if (pthread_mutex_init(&mutex_, 0) != 0) {
      throw MutexInitException();
    }
  }

  ~Mutex() {
    pthread_mutex_destroy(&mutex_);
  }

  void lock() {
    if (pthread_mutex_lock(&mutex_) != 0) {
      throw MutexLockException();
    }
  }

  void unlock() {
    if (pthread_mutex_unlock(&mutex_) != 0) {
      throw MutexUnlockException();
    }
  }

private:
  pthread_mutex_t mutex_;
};

La mia domanda è, questo è il modo standard di fare?Perché se il pthread mutex_init chiamata non riesce oggetto mutex è inutilizzabile così la generazione di un'eccezione assicura che il mutex non essere creato.

Devo creare una funzione membro di init per la classe Mutex e chiamata pthread mutex_init all'interno, che restituisce un valore booleano base pthread mutex_init's ritorno?In questo modo non devo utilizzare eccezioni per un basso livello di oggetto.

È stato utile?

Soluzione

Sì, lanciare un'eccezione dal costruttore fallito è il modo standard per farlo. Leggi queste FAQ su Gestione di un costruttore che fallisce per ulteriori informazioni. Avere anche un metodo init () funzionerà, ma tutti coloro che creano l'oggetto di mutex devono ricordare che deve essere chiamato init (). Credo che vada contro il RAII .

Altri suggerimenti

Se si genera un'eccezione da un costruttore, tenere presente che è necessario utilizzare la funzione try / catch sintassi se è necessario rilevare tale eccezione in un elenco di inizializzatori del costruttore.

per es.

func::func() : foo()
{
    try {...}
    catch (...) // will NOT catch exceptions thrown from foo constructor
    { ... }
}

vs.

func::func()
    try : foo() {...}
    catch (...) // will catch exceptions thrown from foo constructor
    { ... }

Il lancio di un'eccezione è il modo migliore di gestire i guasti del costruttore. Dovresti evitare in particolare la costruzione a metà di un oggetto e quindi fare affidamento sugli utenti della tua classe per rilevare errori di costruzione testando variabili flag di qualche tipo.

Su un punto correlato, il fatto che tu abbia diversi tipi di eccezione per gestire gli errori mutex mi preoccupa leggermente. L'ereditarietà è un ottimo strumento, ma può essere utilizzata eccessivamente. In questo caso, preferirei probabilmente un'unica eccezione MutexError, possibilmente contenente un messaggio di errore informativo.

#include <iostream>

class bar
{
public:
  bar()
  {
    std::cout << "bar() called" << std::endl;
  }

  ~bar()
  {
    std::cout << "~bar() called" << std::endl;

  }
};
class foo
{
public:
  foo()
    : b(new bar())
  {
    std::cout << "foo() called" << std::endl;
    throw "throw something";
  }

  ~foo()
  {
    delete b;
    std::cout << "~foo() called" << std::endl;
  }

private:
  bar *b;
};


int main(void)
{
  try {
    std::cout << "heap: new foo" << std::endl;
    foo *f = new foo();
  } catch (const char *e) {
    std::cout << "heap exception: " << e << std::endl;
  }

  try {
    std::cout << "stack: foo" << std::endl;
    foo f;
  } catch (const char *e) {
    std::cout << "stack exception: " << e << std::endl;
  }

  return 0;
}

l'output:

heap: new foo
bar() called
foo() called
heap exception: throw something
stack: foo
bar() called
foo() called
stack exception: throw something

i distruttori non vengono chiamati, quindi se è necessario lanciare un'eccezione in un costruttore, molte cose (ad esempio ripulire?) da fare.

È OK per generare dal costruttore, ma è necessario assicurarsi che l'oggetto è costruito dopo principale ha iniziato e prima di finiture:

class A
{
public:
  A () {
    throw int ();
  }
};

A a;     // Implementation defined behaviour if exception is thrown (15.3/13)

int main ()
{
  try
  {
    // Exception for 'a' not caught here.
  }
  catch (int)
  {
  }
}

Se il tuo progetto si basa generalmente su eccezioni per distinguere i dati errati da quelli positivi, allora lanciare un'eccezione dal costruttore è la soluzione migliore rispetto al non lanciare. Se l'eccezione non viene generata, l'oggetto viene inizializzato in uno stato di zombi. Tale oggetto deve esporre una bandiera che indica se l'oggetto è corretto o meno. Qualcosa del genere:

class Scaler
{
    public:
        Scaler(double factor)
        {
            if (factor == 0)
            {
                _state = 0;
            }
            else
            {
                _state = 1;
                _factor = factor;
            }
        }

        double ScaleMe(double value)
        {
            if (!_state)
                throw "Invalid object state.";
            return value / _factor;
        }

        int IsValid()
        {
            return _status;
        }

    private:
        double _factor;
        int _state;

}

Il problema con questo approccio è dal lato del chiamante. Ogni utente della classe dovrebbe fare un if prima di utilizzare effettivamente l'oggetto. Questa è una richiesta di bug: non c'è niente di più semplice che dimenticare di testare una condizione prima di continuare.

In caso di lancio di un'eccezione dal costruttore, si suppone che l'entità che costruisce l'oggetto si occupi immediatamente dei problemi. I consumatori di oggetti lungo il flusso sono liberi di presumere che l'oggetto sia operativo al 100% dal semplice fatto di averlo ottenuto.

Questa discussione può continuare in molte direzioni.

Ad esempio, usare le eccezioni come una questione di validazione è una cattiva pratica. Un modo per farlo è un modello Try in combinazione con la classe di fabbrica. Se stai già utilizzando le fabbriche, scrivi due metodi:

class ScalerFactory
{
    public:
        Scaler CreateScaler(double factor) { ... }
        int TryCreateScaler(double factor, Scaler **scaler) { ... };
}

Con questa soluzione è possibile ottenere il flag di stato sul posto, come valore di ritorno del metodo factory, senza mai inserire il costruttore con dati errati.

La seconda cosa è se stai coprendo il codice con test automatici. In tal caso, ogni parte di codice che utilizza un oggetto che non genera eccezioni dovrebbe essere coperta con un test aggiuntivo, indipendentemente dal fatto che agisca correttamente quando il metodo IsValid () restituisce false. Questo spiega abbastanza bene che l'inizializzazione di oggetti nello stato di zombi è una cattiva idea.

A parte il fatto che non è necessario lanciare dal costruttore nel tuo caso specifico perché pthread_mutex_lock restituisce effettivamente un EINVAL se il tuo mutex non è stato inizializzato e puoi lanciare dopo la chiamata su lock come avviene in std::mutex:

void
lock()
{
  int __e = __gthread_mutex_lock(&_M_mutex);

  // EINVAL, EAGAIN, EBUSY, EINVAL, EDEADLK(may)
  if (__e)
__throw_system_error(__e);
}

quindi in generale il lancio dai costruttori va bene per errori di acquisizione durante la costruzione e in conformità con RAII (Resource-acquisition-is- Inizializzazione) paradigma di programmazione.

Controlla questo esempio su RAII

void write_to_file (const std::string & message) {
    // mutex to protect file access (shared across threads)
    static std::mutex mutex;

    // lock mutex before accessing file
    std::lock_guard<std::mutex> lock(mutex);

    // try to open file
    std::ofstream file("example.txt");
    if (!file.is_open())
        throw std::runtime_error("unable to open file");

    // write message to file
    file << message << std::endl;

    // file will be closed 1st when leaving scope (regardless of exception)
    // mutex will be unlocked 2nd (from lock destructor) when leaving
    // scope (regardless of exception)
}

Concentrati su queste affermazioni:

  1. static std::mutex mutex
  2. std::lock_guard<std::mutex> lock(mutex);
  3. std::ofstream file("example.txt");

La prima affermazione è RAII e noexcept. In (2) è chiaro che RAII è applicato su lock_guard e in realtà può throw, mentre in (3) ofstream sembra non essere RAII, poiché lo stato degli oggetti deve essere verificato chiamando is_open() che controlla la failbit bandiera.

A prima vista sembra che sia indeciso su cosa sia il modo standard e nel primo caso std::mutex::lock non getta l'inizializzazione, * in contrasto con l'implementazione dell'OP *. Nel secondo caso lancerà tutto ciò che viene lanciato da connect e nel terzo non viene eseguito alcun lancio.

Nota le differenze:

(1) Può essere dichiarato statico e verrà effettivamente dichiarato come variabile membro (2) Non ci si aspetta mai che venga dichiarato come variabile membro (3) Si prevede che venga dichiarato come variabile membro e la risorsa sottostante potrebbe non essere sempre disponibile.

Tutti questi moduli sono RAII ; per risolvere questo, è necessario analizzare RAII .

  • Risorsa: il tuo oggetto
  • Acquisizione (allocazione): l'oggetto che si sta creando
  • Inizializzazione: l'oggetto si trova nel suo stato invariante

Questo non richiede di inizializzare e connettere tutto in costruzione. Ad esempio, quando si crea un oggetto client di rete non lo si connette effettivamente al server al momento della creazione, poiché si tratta di un'operazione lenta con errori. Dovresti invece scrivere una funzione mutex_ per fare proprio questo. D'altra parte è possibile creare i buffer o semplicemente impostarne lo stato.

Pertanto, il problema si riduce alla definizione del tuo stato iniziale. Se nel tuo caso il tuo stato iniziale è mutex deve essere inizializzato allora dovresti lanciare dal costruttore. Al contrario, va bene non inizializzare quindi (come avviene in locked) e definire il proprio stato invariante quando viene creato mutex . In ogni caso, l'invariante non è necessariamente compromesso dallo stato del suo oggetto membro, poiché l'oggetto unlocked muta tra Mutex e Mutex::lock() attraverso i Mutex::unlock() metodi pubblici <=> e <=>.

class Mutex {
private:
  int e;
  pthread_mutex_t mutex_;

public:
  Mutex(): e(0) {
  e = pthread_mutex_init(&mutex_);
  }

  void lock() {

    e = pthread_mutex_lock(&mutex_);
    if( e == EINVAL ) 
    { 
      throw MutexInitException();
    }
    else (e ) {
      throw MutexLockException();
    }
  }

  // ... the rest of your class
};

L'unica volta in cui NON si generano eccezioni dai costruttori è se il progetto ha una regola contro l'utilizzo di eccezioni (ad esempio, Google non ama le eccezioni). In tal caso, non vorrai usare le eccezioni nel tuo costruttore più che altrove e dovresti invece avere un metodo init di qualche tipo.

Aggiungendo a tutte le risposte qui, ho pensato di menzionare, un motivo / scenario molto specifico in cui potresti voler preferire gettare l'eccezione dal metodo Init della classe e non dal Ctor (che ovviamente è il preferito e un approccio più comune).

Menzionerò in anticipo che questo esempio (scenario) presuppone che non si usi " puntatori intelligenti " (ad es. std::unique_ptr) per la tua classe " membri dei dati dei puntatori.

Quindi, al punto : Nel caso, desideri che il Dtor della tua classe " agisca " quando lo invochi dopo (in questo caso) ottieni l'eccezione che il tuo metodo Init() ha gettato - NON DEVI gettare l'eccezione dal Ctor, poiché una chiamata Dtor per Ctor NON viene invocata su " metà cottura " oggetti.

Vedi l'esempio seguente per dimostrare il mio punto:

#include <iostream>

using namespace std;

class A
{
    public:
    A(int a)
        : m_a(a)
    {
        cout << "A::A - setting m_a to:" << m_a << endl;
    }

    ~A()
    {
        cout << "A::~A" << endl;
    }

    int m_a;
};

class B
{
public:
    B(int b)
        : m_b(b)
    {
        cout << "B::B - setting m_b to:" << m_b << endl;
    }

    ~B()
    {
        cout << "B::~B" << endl;
    }

    int m_b;
};

class C
{
public:
    C(int a, int b, const string& str)
        : m_a(nullptr)
        , m_b(nullptr)
        , m_str(str)
    {
        m_a = new A(a);
        cout << "C::C - setting m_a to a newly A object created on the heap (address):" << m_a << endl;
        if (b == 0)
        {
            throw exception("sample exception to simulate situation where m_b was not fully initialized in class C ctor");
        }

        m_b = new B(b);
        cout << "C::C - setting m_b to a newly B object created on the heap (address):" << m_b << endl;
    }

    ~C()
    {
        delete m_a;
        delete m_b;
        cout << "C::~C" << endl;
    }

    A* m_a;
    B* m_b;
    string m_str;
};

class D
{
public:
    D()
        : m_a(nullptr)
        , m_b(nullptr)
    {
        cout << "D::D" << endl;
    }

    void InitD(int a, int b)
    {
        cout << "D::InitD" << endl;
        m_a = new A(a);
        throw exception("sample exception to simulate situation where m_b was not fully initialized in class D Init() method");
        m_b = new B(b);
    }

    ~D()
    {
        delete m_a;
        delete m_b;
        cout << "D::~D" << endl;
    }

    A* m_a;
    B* m_b;
};

void item10Usage()
{
    cout << "item10Usage - start" << endl;

    // 1) invoke a normal creation of a C object - on the stack
    // Due to the fact that C's ctor throws an exception - its dtor
    // won't be invoked when we leave this scope
    {
        try
        {
            C c(1, 0, "str1");
        }
        catch (const exception& e)
        {
            cout << "item10Usage - caught an exception when trying to create a C object on the stack:" << e.what() << endl;
        }
    }

    // 2) same as in 1) for a heap based C object - the explicit call to 
    //    C's dtor (delete pc) won't have any effect
    C* pc = 0;
    try
    {
        pc = new C(1, 0, "str2");
    }
    catch (const exception& e)
    {
        cout << "item10Usage - caught an exception while trying to create a new C object on the heap:" << e.what() << endl;
        delete pc; // 2a)
    }

    // 3) Here, on the other hand, the call to delete pd will indeed 
    //    invoke D's dtor
    D* pd = new D();
    try
    {
        pd->InitD(1,0);
    }
    catch (const exception& e)
    {
        cout << "item10Usage - caught an exception while trying to init a D object:" << e.what() << endl;
        delete pd; 
    }

    cout << "\n \n item10Usage - end" << endl;
}

int main(int argc, char** argv)
{
    cout << "main - start" << endl;
    item10Usage();
    cout << "\n \n main - end" << endl;
    return 0;
}

Ricorderò ancora, che non è l'approccio raccomandato, volevo solo condividere un ulteriore punto di vista.

Inoltre, come potreste aver visto da alcune delle stampe nel codice - si basa sull'articolo 10 nella fantastica " C ++ " più efficace; di Scott Meyers (prima edizione).

Spero che sia d'aiuto.

Saluti,

Guy.

Anche se non ho lavorato in C ++ a livello professionale, a mio avviso, va bene gettare eccezioni dai costruttori. Lo faccio (se necessario) in .Net. Dai un'occhiata a this e questo link. Potrebbe essere di tuo interesse.

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top