resueltos programacion poo orientada objetos libro ejercicios ejemplos destructores constructores codigo clases c++ exception

programacion - Eliminación de excepciones en constructores C++



programacion orientada a objetos c++ ejemplos (6)

Recientemente nos hemos enfrentado al problema de trasladar nuestro framework C ++ a una plataforma ARM que ejecuta uClinux, donde el único compilador compatible con proveedores es GCC 2.95.3. El problema con el que nos hemos encontrado es que las excepciones son extremadamente poco fiables y hacen que todo no se vea atrapado por un hilo no relacionado (!). Esto parece ser un error documentado, es decir, aquí y aquí .

Después de algunas deliberaciones, decidimos eliminar las excepciones por completo, ya que hemos llegado a un punto en el que las excepciones hacen mucho daño a las aplicaciones en ejecución. La principal preocupación ahora es cómo gestionar los casos en los que falló un constructor.

Hemos intentado la evaluación diferida , donde cada método tiene la capacidad de crear instancias de recursos dinámicos y devolver un valor de estado, pero eso significa que cada método de clase tiene que devolver un valor de retorno que genera muchos ifs en el código y es muy molesto en los métodos que generalmente nunca causaría un error.

Buscamos agregar un método de creación estático que devuelve un puntero a un objeto creado o NULL si la creación falló pero eso significa que ya no podemos almacenar objetos en la pila, y aún hay que pasar una referencia a un valor de estado si lo desea para actuar en el error real.

Según la Guía de estilo C ++ de Google, no usan excepciones y solo realizan un trabajo trivial en sus constructores, utilizando un método init para un trabajo no trivial ( Doing Work in Constructors ). Sin embargo, no puedo encontrar nada sobre cómo manejan los errores de construcción al usar este enfoque.

¿Alguien ha intentado aquí eliminar las excepciones y encontrar una buena solución para manejar las fallas de la construcción?


En cuanto a la referencia de Google (no se pudo encontrar cómo manejaron los errores en el constructor):

La respuesta a esa parte es que si solo hacen un trabajo trivial en el constructor, entonces no hay errores. Debido a que el trabajo es trivial, tienen mucha confianza (respaldado por pruebas exhaustivas, estoy seguro) de que las excepciones simplemente no serán arrojadas.


Generalmente terminas con un código como este para objetos en la pila:

MyClassWithNoThrowConstructor foo; if (foo.init(bar, baz, etc) != 0) { // error-handling code } else { // phew, we got away with it. Now for the next object... }

Y esto para objetos en el montón. Supongo que anulas el operador global nuevo con algo que devuelve NULL en lugar de arrojar, para ahorrarte recordando usar nothrow new en todos lados:

MyClassWithNoThrowConstructor *foo = new MyClassWithNoThrowConstructor(); if (foo == NULL) { // out of memory handling code } else if (foo->init(bar, baz, etc) != 0) { delete foo; // error-handling code } else { // success, we can use foo }

Obviamente, si es posible, use punteros inteligentes para evitar tener que recordar las eliminaciones, pero si su compilador no admite las excepciones correctamente, entonces puede tener problemas para obtener Boost o TR1. No lo sé.

También es posible que desee estructurar la lógica de manera diferente, o abstraer la combinación de new e init, para evitar el "código de flecha" profundamente anidado cuando maneje objetos múltiples, y para hacer que el manejo de errores entre los dos casos de falla sea común. Lo anterior es solo la lógica básica en su forma más minuciosa.

En ambos casos, el constructor establece todo a los valores predeterminados (puede tomar algunos argumentos, siempre que lo que haga con esos argumentos no pueda fallar, por ejemplo si solo los almacena). El método init puede hacer el trabajo real, que puede fallar, y en este caso devuelve 0 éxito o cualquier otro valor por falla.

Probablemente necesites forzar que todos los métodos de inicio en toda la base de códigos denuncien errores de la misma manera: no quieres que 0 regrese o un código de error negativo, algunos devuelven 0 éxito o un código de error positivo, algunos regresan bool, otros regresan un objeto por valor que tiene campos que explican la falla, algunos configuran errno global, etc.

Tal vez puedas echar un vistazo rápido a algunos documentos API de la clase Symbian en línea. Symbian usa C ++ sin excepciones: tiene un mecanismo llamado "Leave" que lo compensa parcialmente, pero no es válido para Leave from a constructor, por lo que tiene el mismo problema básico en términos de diseño de constructores que no fallan y diferir fallas operaciones para iniciar rutinas. Por supuesto, con Symbian, se permite que la rutina init se vaya, por lo que la persona que llama no necesita el código de manejo de errores que indiqué anteriormente, pero en términos de división de trabajo entre un constructor C ++ y una llamada init adicional, es lo mismo.

Los principios generales incluyen:

  • Si su constructor quiere obtener un valor de algún modo que pueda fallar, difiéralo al init y deje el valor default-initialised en el ctor.
  • Si su objeto contiene un puntero, configúrelo como nulo en el ctor y ajústelo "correctamente" en el init.
  • Si su objeto contiene una referencia, cámbielo a un puntero (inteligente) para que pueda comenzar con nulo, o haga que la persona que llama pase el valor al constructor como un parámetro en lugar de generarlo en el ctor.
  • Si tu constructor tiene miembros del tipo de objeto, entonces estás bien. Sus codificadores tampoco lanzarán, por lo que está perfectamente bien construir tus miembros (y clases base) en la lista de inicializadores de la forma habitual.
  • Asegúrese de hacer un seguimiento de lo que está configurado y lo que no, para que el destructor funcione cuando falla el init.
  • Todas las funciones que no sean constructores, destructor e init pueden asumir que init ha tenido éxito, siempre que documente para su clase que no es válido llamar a ningún método que no sea init hasta que init haya sido llamado y haya tenido éxito.
  • Puede ofrecer múltiples funciones init, que a diferencia de los constructores pueden llamarse entre sí, de la misma manera que para algunas clases ofrecería múltiples constructores.
  • No puede proporcionar conversiones implícitas que pueden fallar, por lo que si su código actualmente se basa en conversiones implícitas que generan excepciones, debe rediseñar. Lo mismo ocurre con la mayoría de las sobrecargas del operador, ya que sus tipos de retorno están restringidos.

Podría usar una bandera para realizar un seguimiento de si el constructor falló. Es posible que ya tenga una variable miembro que solo sea válida si el constructor tiene éxito, por ej.

class MyClass { public: MyClass() : m_resource(NULL) { m_resource = GetResource(); } bool IsValid() const { return m_resource != NULL; } private: Resource * m_resource; }; MyClass myobj; if (!myobj.IsValid()) { // error handling goes here }


Si realmente no puede usar excepciones, también puede escribir una macro de construcción haciendo lo que onebyone propuso siempre. Así que no te metes en la molestia de hacer este ciclo de creación / init / if todo el tiempo y lo más importante, nunca olvides inicializar un objeto.

struct error_type { explicit error_type(int code):code(code) { } operator bool() const { return code == 0; } int get_code() { return code; } int const code; }; #define checked_construction(T, N, A) / T N; / if(error_type const& error = error_type(N.init A))

La estructura error_type invertirá la condición, de modo que los errores se verifiquen en la parte else del if. Ahora escriba una función init que devuelva 0 en caso de éxito o cualquier otro valor que indique el código de error.

struct i_can_fail { i_can_fail() { // constructor cannot fail } int init(std::string p1, bool p2) { // init using the given parameters return 0; // successful } }; void do_something() { checked_construction(i_can_fail, name, ("hello", true)) { // alright. use it name.do_other_thing(); } else { // handle failure std::cerr << "failure. error: " << error.get_code() << std::endl; } // name is still in scope. here is the common code }

Puede agregar otras funciones a error_type , por ejemplo, cosas que error_type lo que significa el código.



Supongo que, en gran medida, depende del tipo de excepciones que se producen normalmente. Mi suposición es que están principalmente relacionados con los recursos. Si este es el caso, una solución que he utilizado anteriormente en un sistema C incrustado fue asignar / comprometer todos los recursos potencialmente necesarios al inicio del programa. Por lo tanto, sabía que todos los recursos necesarios estaban disponibles en el momento de la ejecución en lugar de durante la ejecución. Es una solución codiciosa que puede interferir con la interoperabilidad con otro software, pero funcionó bastante bien para mí.