2009-05-01 26 views
222

Estoy teniendo un debate con un compañero de trabajo acerca de arrojar excepciones de los constructores, y pensé que me gustaría recibir algunos comentarios.Lanzando excepciones de los constructores

¿Está bien arrojar 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 algo como esto:

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 forma 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 función miembro init 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.

+0

Otro enlace sobre el tema relacionado: http://www.writeulearn.com/exception-constructor/ –

+0

Bueno, está bien tirar desde los ctors tanto como desde cualquier otra función, dicho esto, se debe lanzar con cuidado de cualquier función. – g24l

+3

Algo no relacionado: ¿por qué no eliminar los métodos de bloqueo/desbloqueo, y bloquear directamente el mutex en el constructor y desbloquear en el destructor?De esta manera, simplemente declarando una variable automática en un alcance automáticamente bloquear/desbloquear, sin necesidad de ocuparse de excepciones, devoluciones, etc ... Ver 'std :: lock_guard' para una implementación similar. –

Respuesta

207

Sí, arrojar una excepción del constructor fallido es la forma estándar de hacerlo. Lea estas preguntas frecuentes sobre Handling a constructor that fails 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.

+13

En la mayoría de las situaciones. No olvides cosas como std :: fstream. En caso de falla, aún crea un objeto, pero como siempre estamos probando el estado del objeto, normalmente funciona bien. Por lo tanto, es posible que no sea necesario tirar un objeto que tiene un estado natural probado bajo un uso normal. –

+1

@Widor: Gracias por revisar mi edición sugerida no. 278978. ¿Puedo hacer una pregunta más relacionada con la edición? La respuesta a la que se adjunta este comentario tiene un hipervínculo desactualizado. Para solucionarlo, quiere cambiar exactamente un carácter, reemplazando "# faq-17.2" con "# faq-17.8" en la URL. Sin embargo, el software de Stackoverflow requiere que una edición enviada por un usuario de poca reputación como yo cambie al menos seis caracteres. Obviamente, el enlace roto quiere ser reparado, y simplemente no es una solución de seis caracteres. ¿Sabes cómo puedo solucionarlo, por favor? – thb

+4

No realmente, en este caso específico, tenga en cuenta que su destructor Mutex nunca será llamado, posiblemente filtrando el mutex pthread. La solución a eso es usar un puntero inteligente para el pthread mutex, mejor aún usar boost mutexes o std :: mutex, no hay razón para seguir usando antiguas construcciones de SO de estilo funcional cuando hay mejores alternativas. –

0

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. Consulte this y this enlace. Puede ser de tu interés.

+10

.NET no es C++, tampoco JAVA. El mecanismo de lanzamiento no es el mismo y los costos son diferentes. – g24l

30

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 los 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.

+0

Quisiera repetir el punto de Neil sobre la jerarquía de excepciones: un solo MutexError probablemente sea una mejor opción a menos que específicamente desee manejar un error de bloqueo de forma diferente. Si tiene demasiados tipos de excepción, atraparlos a todos puede ser tedioso y propenso a errores. – markh44

+0

Acepto que un tipo de excepción mutex es suficiente. Y esto también hará que el manejo de errores sea más intuitivo. – lkristjansen

85

Si lanza una excepción desde un constructor, tenga en cuenta que debe utilizar la sintaxis try/catch de la función si necesita detectar esa excepción en una lista de inicializadores de 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 
    { ... } 
+25

Debe tenerse en cuenta que las excepciones surgidas de la construcción de un objeto secundario no se pueden suprimir: http://www.gotw.ca/gotw/066.htm –

10

Está bien para tirar de su constructor, pero debe asegurarse de que el objeto se construye después de principal ha iniciado y antes de que acabados:

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) 
    { 
    } 
} 
2

La única vez que NO arrojaría excepciones de los constructores es si su proyecto tiene un descartar el uso de excepciones (por ejemplo, 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.

+0

Puede que le interese la larga discusión sobre las directrices de Google en http://groups.google.com/group/comp.lang.c++moderated/browse_thread/thread/d7b0a5c663467471/2e5707c44726925a?lnk=gst&q=google#2e5707c44726925a –

+4

Debate interesante. Mi opinión personal es que debe usar excepciones solo cuando diseña la estructura de manejo de errores del programa para aprovecharlas. Si intenta hacer un manejo de errores después de escribir el código, o trata de calzar las excepciones en programas que no fueron escritos para ellos, solo va a llevar a try/catch EVERYWHERE (eliminando las ventajas de las excepciones) o a programas que fallan en el menos pequeño error. Me ocupo de ambos todos los días y no me gusta. –

3

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 es del 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 arrojar una excepción del 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, utilizar las 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, después de escribir dos métodos:

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

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

Lo segundo es si está cubriendo el código con pruebas automáticas. 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.

3

Aparte del hecho de que no es necesario tirar desde el constructor en su caso específico, porque pthread_mutex_lock actually returns an EINVAL if your mutex has not been initialized y se puede tirar después de la llamada a 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); 
} 

continuación, en general arrojando desde constructores está bien para adquisición errores durante la construcción, y de acuerdo con RAII (Recurso-adquisición-es-Inicialización) paradigma de programación.

Marque esta example on 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) 
} 

Enfoque sobre estos estados:

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

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

A primera vista parece que no se ha decidido sobre lo que la forma estándar y en el primer caso std::mutex no lanza en la inicialización, * en contraste con la implementación OP *. En el segundo caso arrojará lo que sea que se arroje desde std::mutex::lock, y en el tercero no hay lanzamiento en absoluto.

notar las diferencias:

(1) puede ser declarado estática, y realmente será declarada como una variable miembro (2) nunca realmente se espera para ser declarado como una variable miembro (3) es se espera que se declare como una variable miembro, y el recurso subyacente puede no estar siempre disponible.

Todos estos formularios son RAII; para resolver esto, uno debe analizar RAII.

  • de recursos: su objeto
  • Adquisición (asignación): usted objeto que está siendo creado
  • inicialización: el objeto está en su estado invariante

Esto no requiere que para inicializar y conecta 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 connect para hacer justamente eso. Por otro lado, puede crear los búferes o simplemente establecer su estado.

Por lo tanto, su problema se reduce a la definición de su estado inicial. Si en su caso su estado inicial es , se debe inicializar mutex y luego debe lanzar desde el constructor. Por el contrario, está bien no inicializar entonces (como se hace en std::mutex), y definir su estado invariable como 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 a través de Mutex, 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 
}; 
8
#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; 
} 

la salida:

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

los destructores no son llamados, por lo que si una excepción deben ser arrojados en un constructor, un montón de cosas (por ejemplo, limpiar?) Para hacer .

+0

Muy buen punto. Me sorprende que ninguna otra respuesta aborde este tipo de pérdida. – Carlton

+3

Debería utilizar std :: unique_ptr o similar. Destructor de miembros se llama si se lanza una excepción durante la construcción, pero los punteros simples no tienen ninguno. Reemplace 'bar * b' con' std :: unique_ptr b' (deberá eliminar el 'eliminar b;' y agregar el encabezado '') y ejecutar nuevamente. – cbuchart

+1

Este comportamiento es bastante sensato. Si el constructor ha fallado (no se completó con éxito) ¿por qué debería invocarse el destructor? No tiene nada que limpiar y si tratara de limpiar objetos que ni siquiera se han instanciado correctamente (piense en algunos indicadores), causará muchos más problemas, innecesariamente. – zar

Cuestiones relacionadas