Pregunta

Estoy teniendo un debate con un compañero de trabajo sobre cómo generar excepciones por parte de los constructores y pensé que me gustaría recibir comentarios.

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

Digamos que estoy envolviendo un mutex POSIX en una clase, se vería 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 forma estándar de hacerlo?Porque si el pthread mutex_init la llamada falla, el objeto mutex no se puede utilizar, 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? pthread mutex_init dentro del cual devolvería un bool basado en pthread mutex_init¿El regreso?De esta manera no tengo que usar excepciones para un objeto de tan bajo nivel.

¿Fue útil?

Solución

Sí, lanzar una excepción del constructor fallido es la forma estándar de hacerlo. Lea estas preguntas frecuentes sobre 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 deben recordar que se debe llamar a init (). Creo que va en contra del RAII principio.

Otros consejos

Si arroja una excepción de un constructor, tenga en cuenta que necesita usar la función sintaxis try / catch si necesita capturar esa excepción en una lista de inicializador de constructor.

por ejemplo

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 construir a medias un objeto y luego confiar en los usuarios de su clase para detectar fallas en la construcción probando variables de bandera de algún tipo.

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

#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 se llaman, por lo que si se necesita lanzar una excepción en un constructor, se deben hacer muchas cosas (por ejemplo, ¿limpiar?).

Está bien tirar desde su constructor, pero debe asegurarse de que su objeto se construye después de que main ha comenzado y antes 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)
  {
  }
}

Si su proyecto generalmente se basa en excepciones para distinguir datos incorrectos de datos buenos, entonces lanzar una excepción del constructor es una mejor solución que no arrojar. Si no se produce una excepción, 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 if antes de usar el objeto. Esta es una llamada a los errores: no hay nada más simple que olvidar probar una condición antes de continuar.

En caso de lanzar una excepción del constructor, se supone que la entidad que construye el objeto se ocupará de los problemas de inmediato. Los consumidores de objetos que se encuentran más abajo pueden asumir que el objeto es 100% operativo por el simple 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 valor de retorno del método de fábrica, sin tener que ingresar al constructor con datos incorrectos.

La segunda cosa es si está cubriendo el código con pruebas automatizadas. En ese caso, cada pieza de código que use un objeto que no arroje excepciones debería cubrirse con una prueba adicional: si funciona correctamente cuando el método IsValid () devuelve falso. Esto explica bastante bien que inicializar objetos en estado zombie es una mala idea.

Aparte de la hecho de que no es necesario tirar del constructor en su caso específico porque pthread_mutex_lock en realidad devuelve un EINVAL si su mutex no ha sido inicializado y puedes 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);
}

entonces en general tirar de los constructores está bien para adquisición errores durante la construcción, y en cumplimiento de RAII Paradigma de programación (adquisición de recursos es inicialización).

Mira esto 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<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)
}

Concéntrese en estas declaraciones:

  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 is_open() que comprueba el failbit bandera.

A primera vista parece que no están decididos sobre cuál será el manera estándar 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á lo que sea arrojado desde std::mutex::lock, y en el tercero no hay ningún lanzamiento.

Note las diferencias:

(1) Se puede declarar estático y, de hecho, se declarará como una variable miembro (2) En realidad, nunca se esperará que se declare como una variable miembro (3) Se espera que se declare como una variable miembro, y es posible que el recurso subyacente no siempre esté disponible.

Todas estas formas son RAII;Para resolver esto hay que analizar RAII.

  • Recurso :tu objeto
  • Adquisición (asignación):usted se opone a que se cree
  • Inicialización:tu objeto está en su estado invariante

Esto no requiere que inicialice y conecte todo en la construcción.Por ejemplo, cuando creara un objeto de cliente de red, en realidad no lo conectaría al servidor al momento de la creación, ya que es una operación lenta con fallas.En lugar de eso, escribirías un connect función para hacer precisamente eso.Por otro lado, podrías crear los buffers o simplemente establecer su estado.

Por lo tanto, su problema se reduce a definir su estado inicial.Si en tu caso tu estado inicial es el mutex debe ser inicializado entonces deberías tirar desde el constructor.Por el contrario, está bien no inicializar entonces (como se hace en std::mutex ), y defina su estado invariante como se crea el mutex .En cualquier caso, el invariante no se ve necesariamente comprometido por el estado de su objeto miembro, ya que el mutex_ objeto muta entre locked y unlocked a través de Mutex 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, Google no le gustan las excepciones). En ese caso, no querría usar excepciones en su constructor más que en cualquier otro lugar, y tendría que tener un método init de algún tipo en su lugar.

Agregando a todas las respuestas aquí, pensé mencionar, un motivo / escenario muy específico en el que tal vez quieras preferir lanzar la excepción del método Init de la clase y no del Ctor (que por supuesto es el preferido y un enfoque más común).

Mencionaré de antemano que este ejemplo (escenario) supone que no utiliza " punteros inteligentes " (es decir, std::unique_ptr) para su clase ' s puntero (s) miembros de datos.

Entonces, hasta el punto : en caso de que desee que el Dtor de su clase " tome medidas " cuando lo invoca después (para este caso) detecta la excepción que arrojó su método Init(): NO DEBE lanzar la excepción del Ctor, porque una invocación de Dtor para Ctor NO se invoca en " medio cocido " objetos.

Vea el siguiente ejemplo para demostrar mi 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;
}

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

Además, como puede haber visto en parte de la impresión en el código, se basa en el elemento 10 en el fantástico " Más eficaz C ++ " por Scott Meyers (primera edición).

Espero que ayude.

Saludos,

Guy.

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

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top