Lanzar excepciones de los constructores

Estoy teniendo un debate con un compañero de trabajo acerca de arrojar excepciones de los constructores, y pensé que me gustaría recibir algún comentario.

¿Está bien lanzar excepciones de los constructores desde el punto de vista del diseño?

Digamos que estoy envolviendo un mutex posix en una clase, se vería más o menos así:

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_; }; 

Mi pregunta es, ¿es esta la manera estándar de hacerlo? Porque si falla la llamada pthread mutex_init, el objeto mutex no se puede usar, por lo que lanzar una excepción garantiza que no se creará el mutex.

¿Debería crear una init de función miembro para la clase Mutex y llamar a pthread mutex_init dentro de la cual se devolvería un bool basado en el retorno de pthread mutex_init? De esta forma, no tengo que usar excepciones para un objeto de tan bajo nivel.

    Sí, arrojar una excepción del constructor fallido es la forma estándar de hacerlo. Lea esta pregunta frecuente sobre el manejo de un constructor que falla para obtener más información. Tener un método init () también funcionará, pero todos los que crean el objeto de mutex tienen que recordar que se debe llamar a init (). Siento que va en contra del principio RAII .

    Si lanza una excepción desde un constructor, tenga en cuenta que necesita usar la syntax try / catch de la función si necesita capturar esa excepción en una lista de inicializadores del constructor.

    p.ej

     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 { ... } 

    Lanzar una excepción es la mejor manera de lidiar con la falla del constructor. En particular, debe evitar la mitad de la construcción de un objeto y luego confiar en los usuarios de su clase para detectar fallas en la construcción al probar variables de bandera de algún tipo.

    En un punto relacionado, el hecho de que tenga varios tipos de excepciones diferentes para tratar con errores mutex me preocupa un poco. La herencia es una gran herramienta, pero puede ser utilizada en exceso. En este caso, probablemente preferiría una sola excepción MutexError, que posiblemente contenga un mensaje de error informativo.

    Está bien lanzar desde su constructor, pero debe asegurarse de que su objeto se construye después de que main ha comenzado y antes de que finalice:

     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) { } } 
     #include  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; } 

    La salida:

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

    no se invocan los destructores, por lo que si se debe lanzar una excepción en un constructor, hay que hacer muchas cosas (por ejemplo, ¿limpiar?).

    Si su proyecto generalmente se basa en excepciones para distinguir los datos incorrectos de los buenos, entonces lanzar una excepción desde el constructor es una mejor solución que no lanzar. Si no se lanza la excepción, entonces el objeto se inicializa en un estado zombie. Tal objeto necesita exponer una bandera que dice si el objeto es correcto o no. Algo como esto:

     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; } 

    El problema con este enfoque está en el lado de la persona que llama. Todos los usuarios de la clase tendrían que hacer un si antes de usar realmente el objeto. Este es un llamado para detectar errores: no hay nada más sencillo que olvidarte de probar una condición antes de continuar.

    En caso de lanzar una excepción desde el constructor, se supone que la entidad que construye el objeto se ocupa de los problemas de inmediato. Los consumidores de objetos en el futuro son libres de asumir que el objeto es 100% operativo por el mero hecho de que lo obtuvieron.

    Esta discusión puede continuar en muchas direcciones.

    Por ejemplo, usar excepciones como una cuestión de validación es una mala práctica. Una forma de hacerlo es un patrón de prueba junto con la clase de fábrica. Si ya está utilizando fábricas, escriba dos métodos:

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

    Con esta solución puede obtener el indicador de estado en el lugar, como un valor de retorno del método de fábrica, sin tener que ingresar al constructor con datos incorrectos.

    Lo segundo es si está cubriendo el código con pruebas automatizadas. En ese caso, cada pieza de código que utiliza un objeto que no arroja excepciones debería cubrirse con una prueba adicional: si actúa correctamente cuando el método IsValid () devuelve falso. Esto explica bastante bien que la inicialización de objetos en estado zombie es una mala idea.

    Además del hecho de que no necesita lanzar desde el constructor en su caso específico porque pthread_mutex_lock realmente devuelve un EINVAL si su mutex no se ha inicializado y puede lanzar después de la llamada para lock como se hace en std::mutex :

     void lock() { int __e = __gthread_mutex_lock(&_M_mutex); // EINVAL, EAGAIN, EBUSY, EINVAL, EDEADLK(may) if (__e) __throw_system_error(__e); } 

    entonces, en general, arrojar desde constructores está bien para los errores de adquisición durante la construcción, y de acuerdo con el paradigma de progtwigción RAII (adquisición de recursos-es-inicialización).

    Verifique este ejemplo en 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 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) } 

    Enfóquese en estas declaraciones:

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

    La primera statement es RAII y no noexcept . En (2) está claro que RAII se aplica a lock_guard y realmente puede throw , mientras que en (3) ofstream no parece ser RAII, ya que el estado de los objetos debe verificarse al llamar a is_open() que verifica el indicador de failbit .

    A primera vista, parece que no está decidido de qué manera es la norma y en el primer caso std::mutex no incluye la inicialización *, a diferencia de la implementación de OP *. En el segundo caso arrojará todo lo que se lanza desde std::mutex::lock , y en el tercero no hay throw en absoluto.

    Observe las diferencias:

    (1) Puede declararse estático, y en realidad se declarará como una variable miembro (2) Nunca se espera que se declare realmente como una variable miembro (3) Se espera que se declare como una variable miembro, y el recurso subyacente puede no siempre estará disponible.

    Todas estas formas son RAII ; para resolver esto, uno debe analizar RAII .

    • Recurso: su objeto
    • Adquisición (asignación): objeta ser creado
    • Inicialización: su objeto está en su estado invariable

    Esto no requiere que inicie y conecte todo en la construcción. Por ejemplo, cuando crearía un objeto de cliente de red, en el momento de la creación no lo conectaría al servidor, ya que es una operación lenta con fallas. En su lugar, escribiría una función de connect para hacer exactamente eso. Por otro lado, puede crear los búferes o simplemente establecer su estado.

    Por lo tanto, su problema se reduce a definir su estado inicial. Si en su caso su estado inicial es mutex debe inicializarse, entonces debe lanzar desde el constructor. Por el contrario, está bien no inicializar entonces (como se hace en std::mutex ), y definir su estado invariable cuando se crea mutex . En cualquier caso, el invariante no está comprometido necesariamente por el estado de su objeto miembro, ya que el objeto mutex_ muta entre locked y unlocked través de los métodos públicos Mutex::lock() y Mutex::unlock() .

     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 }; 

    La única vez que NO arrojaría excepciones de los constructores es si su proyecto tiene una regla contra el uso de excepciones (por ejemplo, a Google no le gustan las excepciones). En ese caso, no querría usar excepciones en su constructor más que en ningún otro lado, y en su lugar tendría que tener algún tipo de método init.

    Agregando a todas las respuestas aquí, pensé mencionar, una razón / escenario muy específico en el que podría preferir arrojar la excepción del método init de la clase y no del ctor (que por supuesto es el enfoque preferido y más común) .

    Mencionaré de antemano que este ejemplo (escenario) asume que no se usan “punteros inteligentes” (es decir, std::unique_ptr ) para los miembros de datos de los punteros de su clase.

    Así que al punto: en caso, desea que el maestro de su clase “tome medidas” cuando lo invoque después (para este caso) capte la excepción que lanzó su método Init() – NO DEBE arrojar la excepción de el ctor, porque dtor para dtor’s NO se invoca en objetos “a medio cocer”.

    Vea el ejemplo a continuación para demostrar mi punto:

     #include  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 excpetiopn when trying to create a C object on the stack:" << e.what() << endl; } } // 2) same as io 1) for a heap based C object - the explict 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; } 

    Como dije, volveré a mencionar que no es el enfoque recomendado, solo quería compartir un punto de vista adicional.

    Espero eso ayude.

    Aclamaciones,

    Chico.

    Aunque no he trabajado C ++ a nivel profesional, en mi opinión, está bien arrojar excepciones de los constructores. Hago eso (si es necesario) en .Net. Mira esto y este enlace. Puede ser de tu interés.