Otro de mis ladrillos sobre C++. Esta vez sobre lanzamiento de excepciones polimórficas en C++ sobre Linux. Vamos al lío:
Para ponernos en antecedentes, digamos que tenemos una aplicación multihilo escrita en C++ en Linux. Cuando en uno de los hilos se produce una excepción, esta debe ser atrapada (try { ... } catch(...) { ... }) dentro del propio hilo. En caso de que no se haga así, la excepción se propaga al hilo principal (main()) sin posibilidad de atraparla, con lo que la aplicación finaliza.
Para resumir este punto: las excepciones no se propagan entre diferentes hilos de ejecución por lo que hay que atraparlas y procesarlas dentro del propio hilo.
Pero ¿qué pasa cuando queremos que una excepción sí se propague a otros hilos? Es decir, que un error que se produce en un hilo se notifique a otro hilo para que tome medidas. Por ejemplo, tenemos un hilo que gestiona las comunicaciones y otro que gestiona la interfaz. Cuando se produce un error de comunicación es la interfaz la que tiene que notificar al usuario de dicho error.
Soluciones existen varias. Una de ellas es enviar un mensaje con la excepción que se produce al hilo que la procesa aunque hay que tener en cuenta que debe haber una arquitectura de paso de mensajes entre hilos previa.
Por ejemplo, en pseudocódigo, el hilo de comunicaciones:
void
hilo_comunicaciones() {
while(mensaje = cola_mensajes.getMensaje()) {
try {
enviar_mensaje(mensaje);
mensaje = recibir_respuesta();
} catch(Exception& e) {
notificar_interfaz(e);
}
}
}
Y ahora el hilo de proceso de las notificaciones en la interfaz:
void
hilo_proceso_notificaciones_error_comunicaciones(Exception& e) {
try {
throw e;
} catch(ComunicationException& e) {
} catch(TimeOutException& e) {
} catch(ProtocolException& e) {
} catch(…) {
}
}
El problema del proceso de procesado de excepciones en la interfaz es que al relanzar la excepción que se recibe como parámetro (throw e;) esta no es la excepción recibida en el hilo de comunicaciones, es decir, no hay polimorfismo por lo que siempre se procesa como una excepción general en lugar de la correspondiente.
La opción de usar typeid(e).name() en la excepción para procesar su tipo no es viable porque es demasiado costosa (el operador typeid es bastante costoso) y porque el nombre de la excepción no es estándar, esto es, en GNU C++ (g++) el nombre está “retorcido”, mangled en inglés.
La opción recomendable es añadir a la excepción base de tu aplicación, teniendo en cuenta que para tu aplicación has desarrollado una jerarquía de excepciones acorde con su funcionalidad, un método virtual que implementen las excepciones derivadas donde, simplemente, se lancen a sí mismas.
Un código de ejemplo sería el siguiente:
class Exception : public runtime_error {
public:
Exception(const string msg) : runtime_error(msg) {}
virtual void raise() { throw *this; }
};
class AccessDeniedException : public Exception {
public:
AccessDeniedException(const string msg) : Exception(msg) {}
virtual void raise() { throw *this; }
};
Con esta forma de declarar las excepciones, el código de gestión de las notificaciones de error en la interfaz quedaría:
void
hilo_proceso_notificaciones_error_comunicaciones(Exception& e) {
try {
e.raise();
} catch(ComunicationException& e) {
} catch(TimeOutException& e) {
} catch(ProtocolException& e) {
} catch(…) {
}
}
Con lo que funcionaría sin ningún problema ya que la excepción se lanza de forma polimórfica al usar un método virtual en la clase base que implementan todas las clases derivadas.
Para hacer una prueba se puede descargar un archivo con un programa de ejemplo (1,4 KB). Para compilarlo basta con ejecutar el comando g++ -o test exception-test.cpp.
Y por ir un poco más allá, se podría pensar que para implementar todas las excepciones que usa una aplicación habría que escribir bastante código, además, la mayoría sería repetido; lo único que cambiaría sería el nombre de la excepción y del constructor, el resto es todo igual.
Bueno, pues para eso están las macros de C: se puede crear una macro a la que se le pase como parámetro el nombre de tu nueva excepción y que ella genere todo el código restante. Pero no olvidéis que las macros son malignas en 4 formas diferentes.