c++ - Lanzar excepciones de constructores.
exception throw (10)
Agregando a todas las respuestas aquí, pensé mencionar, una razón / escenario muy específico en el que es posible que prefiera lanzar 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 usted no usa "punteros inteligentes" (es decir, std::unique_ptr
) para los miembros de datos de puntero (s) de su clase.
Entonces, al punto : en caso de que desees que el Dtor de tu clase "actúe" cuando lo invocas después de (para este caso) detectas la excepción que tu método Init()
lanzó, no DEBES lanzar la excepción desde el Ctor, por lo que una invocación de Dtor para Ctor NO se invoca en objetos "a medias".
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;
}
Mencionaré de nuevo, que no es el enfoque recomendado, solo quería compartir un punto de vista adicional.
Además, como puede haber visto en algunas de las letras del código, se basa en el artículo 10 en el fantástico "Más efectivo C ++" de Scott Meyers (1ª edición).
Espero eso ayude.
Aclamaciones,
Chico.
Estoy teniendo un debate con un compañero de trabajo sobre el lanzamiento de excepciones de los constructores, y pensé que me gustaría 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 algo 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 la llamada pthread mutex_init
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 de la cual devolvería un bool basado en el pthread mutex_init
de pthread mutex_init
? De esta manera no tengo que usar excepciones para un objeto de tan bajo nivel.
Aparte del hecho de que no necesita lanzar desde el constructor en su caso específico porque pthread_mutex_lock
realidad 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);
}
luego, en general, el lanzamiento de constructores está bien para los errores de adquisición durante la construcción, y en cumplimiento con el paradigma de programación RAII (Recurso de adquisición es inicialización).
Mira 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<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)
}
Centrarse en estas declaraciones:
-
static std::mutex mutex
-
std::lock_guard<std::mutex> lock(mutex);
-
std::ofstream file("example.txt");
La primera afirmación es RAII y noexcept
. En (2) está claro que RAII se aplica en lock_guard
y que en realidad puede throw
, mientras que en (3) parece que RAII no es RAII, ya que el estado de los objetos debe verificarse llamando a is_open()
que verifica el indicador de failbit
.
A primera vista, parece que no está decidido en lo que es la forma estándar y, en el primer caso, std::mutex
no std::mutex
inicialización, * a diferencia de la implementación de OP *. En el segundo caso lanzará lo que sea lanzado desde std::mutex::lock
, y en el tercero no hay ningún lanzamiento.
Note las diferencias:
(1) Se puede declarar como estático, y en realidad se declarará como una variable miembro (2) Nunca se esperará que se declare 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, hay que analizar RAII .
- Recurso: tu objeto
- Adquisición (asignación): tu objeto está siendo creado
- Inicialización: su 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 crearlo, ya que es una operación lenta con fallas. En lugar de eso, escribirías una función de connect
para hacer 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. En contraste, está bien no inicializar entonces (como se hace en std::mutex
), y definir su estado invariante a medida que se crea el mutex . En cualquier caso, el invariante no se ve 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
};
Está bien lanzar desde su constructor, pero debe asegurarse de que su objeto se construya después de que se haya iniciado el main 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)
{
}
}
La única vez que NO lanzarí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 cualquier otro lugar, y tendría que tener un método de inicio de algún tipo.
Lanzar una excepción es la mejor manera de lidiar con la falla del constructor. Debería evitar particularmente la construcción parcial de un objeto y luego confiar en los usuarios de su clase para detectar fallas en la construcción mediante la prueba de variables de marca de algún tipo.
En un punto relacionado, me preocupa un poco el hecho de que tenga varios tipos de excepción diferentes para tratar los errores de exclusión. La herencia es una gran herramienta, pero puede ser usada en exceso. En este caso, probablemente preferiría una única excepción MutexError, que posiblemente contenga un mensaje de error informativo.
Sí, lanzar una excepción desde el constructor fallido es la forma estándar de hacerlo. Lea estas preguntas frecuentes 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 exclusión mutua deben recordar que se debe llamar a init (). Siento que va en contra del principio RAII .
Si lanza una excepción de un constructor, tenga en cuenta que debe usar la sintaxis de prueba / captura de la función si necesita capturar esa excepción en una lista de inicializadores de constructores.
p.ej
func::func() : foo()
{
try {...}
catch (...) // will NOT catch exceptions thrown from foo constructor
{ ... }
}
contra
func::func()
try : foo() {...}
catch (...) // will catch exceptions thrown from foo constructor
{ ... }
Si su proyecto generalmente se basa en excepciones para distinguir datos malos de datos buenos, entonces lanzar una excepción desde el constructor es una mejor solución que no lanzar. Si no se lanza la excepción, el objeto se inicializa en un estado zombie. Dicho 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. Cada usuario de la clase tendría que hacer un "if" antes de usar el objeto. Este es un llamado a los errores: no hay nada más sencillo 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 encarga 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 Try en conjunto con la clase de fábrica. Si ya estás usando fábricas, escribe 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 erróneos.
Lo segundo es si está cubriendo el código con pruebas automatizadas. En ese caso, cada fragmento de código que utilice un objeto que no arroje excepciones deberá cubrirse con una prueba adicional: si actúa correctamente cuando el método IsValid () devuelve falso. Esto explica bastante bien que inicializar objetos en estado zombie es una mala idea.
#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?).