2008-12-02 24 views
12

Recientemente nos enfrentamos 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, here y here.Eliminación de excepciones en constructores de C++

Después de un poco de deliberación, decidimos eliminar las excepciones por completo, ya que hemos llegado al punto en 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 tratado lazy evaluation, donde cada método tiene la capacidad de crear instancias de recursos dinámicos y devolver un valor de estado, sino que significa que cada método de clase tiene que devolver un valor de retorno que hace que para una gran cantidad de IFS en el código y es muy molesto en métodos que generalmente nunca causarían un error.

Nos miramos a la adición de una estática crear método que devuelve un puntero a un objeto o NULL creado si la creación fracasó pero eso significa que no podemos guardar objetos en la pila más, y todavía hay necesidad de pasar de una referencia a un valor de estado si quiere actuar sobre el error real.

Según la Guía de estilo C++ de Google, do not use exceptions y solo realizan un trabajo trivial en sus constructores, utilizando un método init para trabajos no triviales (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 fallas en la construcción?

Respuesta

13

Generalmente se termina con un código como este para los 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 por objetos en el montón.Asumo que los redefina operador global nuevo con algo que devuelve NULL en lugar de tirar, para ahorrarse recordando a utilizar nothrow nueva en todas partes:

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 le sea posible, utilizar punteros inteligentes para ahorrar 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 forma diferente, o abstracción de la combinación de new e init, para evitar el "código de flecha" profundamente anidado cuando maneje objetos múltiples, y para hacer común el manejo de errores entre dos casos de falla. 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 hace con esos argumentos no puede fallar, por ejemplo, si simplemente 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.

es probable que necesite para hacer cumplir que cada método init lo largo de toda su base de código informa de errores de la misma manera: que haces no quieren algunos regresando 0 éxito o un código de error negativo, algunos 0 éxito devolución o un código de error positivo, algunos devolviendo bool, algunos devolviendo un objeto por valor que tiene campos que explican la falla, algunos configuran errno global, etc.

Quizás pueda 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 abandonar desde un constructor, por lo que tiene el mismo problema básico en términos de diseñar constructores que no fallan y diferir el error 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.

principios generales incluyen:

  • Si el constructor quiere obtener un valor de algún lugar de una manera que puede fallar, que aplazar al inicio y dejar el valor por defecto inicializa en el ctor.
  • Si su objeto tiene un puntero, configúrelo como nulo en el ctor y ajústelo "correctamente" en el init.
  • Si su objeto contiene una referencia, cambie 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 su constructor tiene miembros de tipo de objeto, entonces está bien. Sus codificadores tampoco lanzarán, por lo que está perfectamente bien construir sus 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, el destructor e init pueden suponer que init tuvo é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 varias funciones init, que a diferencia de los constructores pueden llamarse entre sí, del mismo modo 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, entonces debe rediseñar.Lo mismo ocurre con la mayoría de las sobrecargas del operador, ya que sus tipos de retorno están restringidos.
+0

Algunas partes del trabajo de refuerzo pero ya tenemos nuestro propio puntero inteligente, así que eso no es un problema. Hasta ahora no estamos considerando un problema de std :: bad_alloc porque si nos quedamos sin memoria tenemos problemas mayores. ¡Gracias por todas las buenas sugerencias! –

+0

En esta situación, usando if/else anidado para verificar si hay errores, usando la palabra clave goto se puede usar para reducir el depósito de anidación Esto hace que el código sea más fácil para el ojo. Entonces, yo haría "if (something failed) goto error_handler;" – Skizz

+0

Probablemente valga la pena señalar que este patrón se conoce como "construcción en dos fases" – ChrisN

0

En cuanto a la referencia de Google (que no pudo encontrar la forma en que manejan los errores en el constructor):

La respuesta a esa parte es que si sólo lo hacen el 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.

+0

No se trata de cosas que arrojen excepciones, es una cuestión de cómo manejar los errores en los constructores sin excepciones. Lo cual estoy bastante seguro de que es un problema que Google se encontraría tarde o temprano. –

+0

La disciplina para escribir constructores de nothrow no es realmente tan diferente de la disciplina requerida para escribir el intercambio de nothrow: el mismo tipo de análisis "prueba" que no se puede lanzar. Si su función de intercambio tiene que "manejar errores", entonces debe rediseñar su clase, y lo mismo se aplica aquí a los ctors. –

+0

Luego solo s/excepciones/errores /. El punto sigue siendo válido: si solo haces un trabajo trivial, puedes estar razonablemente seguro de que no sucederá. –

0

Supongo que en gran medida, depende de qué tipo de excepciones 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í.

+0

No creo que funcione para nuestra aplicación porque reacciona a los eventos durante el tiempo de ejecución, lo que provocará la creación de instancias de recursos que no conocemos en el momento del lanzamiento. –

-4

Si un constructor solo está haciendo cosas triviales como inicializar variables POD (y llamar implícitamente a otros constructores triviales), entonces posiblemente no pueda fallar. Vea el C++ FQA; ver también why you shouldn't use C++ exceptions.

+1

Ahora bien, si solo una aplicación orientada a objetos pudiera consistir solo en clases POD :( –

+2

Citar C++ FQA tiene poco sentido en este contexto. Fue escrito con el único propósito de convencer a la gente de no usar C++, y no es así. una opción aquí. –

+2

Lo siento, pero no puedo dejar que una pregunta que cita la FQA como una buena fuente se siente en dos upvotes. Es un texto sarcástico (con un serio motivo para que la gente deje de usar C++), y malinterpretarlo puede tener malos resultados. – coppro

0

Si realmente no puede usar excepciones, también puede escribir una macro de construcción haciendo lo que onebyone propuso siempre. Así que no se mete en la molestia de hacer esto creation/init/if ciclo todo el tiempo y más importante, nunca olvida 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 tipo_error invertirá la condición, por lo que los errores se comprueban en la parte else del caso. 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 
} 

Se pueden añadir otras funciones a error_type, por ejemplo, materia que mira hacia arriba lo que significa que el código.

1

Puede usar un indicador para realizar un seguimiento de si el constructor ha fallado. Es posible que ya tenga una variable miembro que solo sea válida si el constructor tiene éxito, p.

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 
}