¿Cómo manejar la falla en el constructor en C ++?

Quiero abrir un archivo en un constructor de clase. Es posible que la apertura pueda fallar, entonces la construcción del objeto no pudo completarse. ¿Cómo manejar esta falla? ¿Lanzar excepción? Si esto es posible, ¿cómo manejarlo en un constructor sin tiro?

Si falla la construcción de un objeto, ejecute una excepción.

La alternativa es horrible. Tendría que crear una bandera si la construcción tuvo éxito y verificarla en todos los métodos.

Quiero abrir un archivo en un constructor de clase. Es posible que la apertura pueda fallar, entonces la construcción del objeto no pudo completarse. ¿Cómo manejar esta falla? ¿Lanzar excepción?

Sí.

Si esto es posible, ¿cómo manejarlo en un constructor sin tiro?

Tus opciones son:

  • rediseña la aplicación para que no necesite constructores que no tiren nada. De hecho, hazlo si es posible
  • agrega una bandera y prueba para una construcción exitosa
    • podría tener cada función de miembro que legítimamente podría ser llamada inmediatamente después de que el constructor pruebe la bandera, lanzando idealmente si está configurada, pero de lo contrario devolvería un código de error
      • Esto es feo y difícil de mantener si tienes un grupo volátil de desarrolladores trabajando en el código.
      • Puede obtener una verificación en tiempo de comstackción de esto haciendo que el objeto se detenga polimórficamente a cualquiera de las dos implementaciones: una construida con éxito y una versión con errores constantes, pero que presenta el uso del almacenamiento dynamic y los costos de rendimiento.
    • Puede trasladar la carga de verificar el indicador desde el código llamado al destinatario documentando el requisito de que invoque alguna función “is_valid ()” o similar antes de usar el objeto: de nuevo propenso a errores y feo, pero aún más distribuido, no identificable y fuera de control.
      • Puede hacer esto un poco más fácil y más localizado para la persona que llama si admite algo como: if (X x) ... (es decir, el objeto puede evaluarse en un contexto booleano, normalmente proporcionando al operator bool() const o integral similar conversión), pero luego no tiene x en scope para consultar detalles del error. Esto puede ser familiar de, por ejemplo, if (std::ifstream f(filename)) { ... } else ...;
  • hacer que la persona que llama proporcione una transmisión que son responsables de haber abierto … (conocida como Inyección de Dependencia o DI) … en algunos casos, esto no funciona tan bien:
    • aún puede tener errores cuando vaya a usar la secuencia dentro de su constructor, ¿entonces qué?
    • el archivo en sí podría ser un detalle de implementación que debería ser privado para su clase en lugar de estar expuesto a la persona que llama: ¿qué sucede si desea eliminar ese requisito más adelante? Por ejemplo: es posible que haya leído una tabla de búsqueda de resultados precalculados de un archivo, pero que haya realizado sus cálculos tan rápido que no haya necesidad de precalcular: es doloroso (a veces incluso poco práctico en un entorno empresarial) eliminar el archivo en todos los puntos de el uso del cliente, y obliga a una mayor recostackción en lugar de volver a vincularlo.
  • obligar a la persona que llama a proporcionar un búfer a una variable de éxito / falla / condición de error que el constructor establece: por ejemplo, bool worked; X x(&worked); if (worked) ... bool worked; X x(&worked); if (worked) ...
    • esta carga y verbosidad llaman la atención y, con un poco de suerte, hacen que el que llama tenga mucha más conciencia de la necesidad de consultar la variable después de construir el objeto
  • obligue al llamante a construir el objeto a través de alguna otra función que pueda usar códigos de retorno y / o excepciones:
    • if (X* p = x_factory()) ...
    • Smart_Ptr_Throws_On_Null_Deref p_x = x_factory ();
    • X x; // nunca utilizable; if (init_x (& x)) … `
    • etc …

En resumen, C ++ está diseñado para proporcionar soluciones elegantes a este tipo de problemas: en este caso, excepciones. Si se restringe artificialmente a usarlos, no espere que haya algo más que haga la mitad de un buen trabajo.

(PD) Me gusta pasar variables que serán modificadas por puntero, como he worked anteriormente, sé que las preguntas más frecuentes lo desalientan pero no están de acuerdo con el razonamiento. No estoy especialmente interesado en el debate a menos que tenga algo no cubierto por las preguntas frecuentes.

Mi sugerencia para esta situación específica es que si no quiere que un constuctor falle porque si no puede abrir un archivo, entonces evite esa situación. Pase un archivo ya abierto al constructor si eso es lo que quiere, entonces no puede fallar …

El nuevo estándar de C ++ redefine esto de muchas maneras, de modo que es hora de revisar esta pregunta.

Las mejores elecciones:

  • Nombre opcional : Tener un constructor privado mínimo y un constructor nombrado: static std::experimental::optional construct(...) . Este último intenta configurar campos de miembros, garantiza invariantes y solo llama al constructor privado si seguramente tendrá éxito. El constructor privado solo rellena los campos de miembros. Es fácil probar el opcional y es económico (incluso la copia se puede ahorrar en una buena implementación).

  • Estilo funcional : la buena noticia es que los constructores (sin nombre) nunca son virtuales. Por lo tanto, puede reemplazarlos con una función de miembro de plantilla estática que, además de los parámetros del constructor, toma dos (o más) lambdas: una si fue exitosa, una si falló. El constructor ‘real’ sigue siendo privado y no puede fallar. Esto puede sonar excesivo, pero los lambda son optimizados maravillosamente por los comstackdores. Incluso puede ahorrar el if de la opción de esta manera.

Buenas elecciones:

  • Excepción : si todo lo demás falla, use una excepción, pero tenga en cuenta que no puede detectar una excepción durante la inicialización estática. Una posible solución es tener el valor de retorno de una función para inicializar el objeto en este caso.

  • Clase de generador : si la construcción es complicada, tenga una clase que haga la validación y posiblemente algún preproceso hasta el punto en que la operación no pueda fallar. Permita que tenga una forma de devolver el estado (sí, función de error). Yo personalmente lo haría astackdo solo, para que la gente no lo pase; entonces déjalo tener un método .build() que construye la otra clase. Si el constructor es amigo, el constructor puede ser privado. Incluso podría tomar algo que solo el constructor puede construir para que esté documentado que este constructor solo debe ser llamado por el constructor.

Malas elecciones: (pero visto muchas veces)

  • Bandera : no estropees tu invariante de clase teniendo un estado “inválido”. Esta es exactamente la razón por la que tenemos optional<> . Piensa en optional que puede ser inválida, T que no puede. Una función (miembro o global) que funciona solo en objetos válidos funciona en T Uno que seguramente devuelve trabajos válidos en T Uno que puede devolver un objeto inválido devuelve optional . Uno que podría invalidar un objeto toma optional& no optional& o optional* . De esta forma, no necesitará verificar todas y cada una de las funciones en las que su objeto es válido (y aquellas if s puede ser un poco caro), pero tampoco fallará en el constructor.

  • Constructor y establecedores predeterminados : Esto es básicamente lo mismo que Bandera, solo que esta vez estás obligado a tener un patrón mutable. Olvídate de los setters, ellos innecesariamente complican tu invariante de clase. Recuerde mantener su clase simple, no simple de construcción.

  • Construcción predeterminada e init() que toma los parámetros de un ctor : Esto no es nada mejor que una función que devuelve un optional<> , pero requiere dos construcciones y arruina tu invariante.

  • Tome bool& succeed : esto era lo que estábamos haciendo antes de optional<> . El motivo optional<> es superior, no puede ignorar (o descuidadamente) la bandera de succeed y continuar utilizando el objeto parcialmente construido.

  • Fábrica que devuelve un puntero : Esto es menos general ya que obliga al objeto a asignarse dinámicamente. O bien devuelve un tipo determinado de puntero administrado (y, por lo tanto, restringe el esquema de asignación / scope) o devuelve un ptr desnudo y los clientes de riesgo pierden. Además, con los esquemas de movimiento en cuanto al rendimiento, esto podría ser menos deseable (los locales, cuando se mantienen en la stack, son muy rápidos y amigables con el caché).

Ejemplo:

 #include  #include  #include  class C { public: friend std::ostream& operator< <(std::ostream& os, const C& c) { return os << c.m_d << " " << c.m_sqrtd; } static std::experimental::optional construct(const double d) { if (d>=0) return C(d, sqrt(d)); return std::experimental::nullopt; } template static auto if_construct(const double d, Success success, Failed failed = []{}) { return d>=0? success( C(d, sqrt(d)) ): failed(); } /*C(const double d) : m_d(d), m_sqrtd(d>=0? sqrt(d): throw std::logic_error("C: Negative d")) { }*/ private: C(const double d, const double sqrtd) : m_d(d), m_sqrtd(sqrtd) { } double m_d; double m_sqrtd; }; int main() { const double d = 2.0; // -1.0 // method 1. Named optional if (auto&& COpt = C::construct(d)) { C& c = *COpt; std::cout < < c << std::endl; } else { std::cout << "Error in 1." << std::endl; } // method 2. Functional style C::if_construct(d, [&](C c) { std::cout << c << std::endl; }, [] { std::cout << "Error in 2." << std::endl; }); } 

Quiero abrir un archivo en un constructor de clase.

Casi ciertamente una mala idea. Muy pocos casos cuando se abre un archivo durante la construcción es apropiado.

Es posible que la apertura pueda fallar, entonces la construcción del objeto no pudo completarse. ¿Cómo manejar esta falla? ¿Lanzar excepción?

Sí, ese sería el camino.

Si esto es posible, ¿cómo manejarlo en un constructor sin tiro?

Haga posible que un objeto completamente construido de su clase pueda ser inválido. Esto significa proporcionar rutinas de validación, usarlas, etc. … ick

Una forma es lanzar una excepción. Otra es tener un funcion ‘bool is_open ()’ o ‘bool is_valid ()’ que devuelve falso si algo salió mal en el constructor.

Algunos comentarios aquí dicen que es incorrecto abrir un archivo en el constructor. Señalaré que ifstream forma parte del estándar C ++ y tiene el siguiente constructor:

 explicit ifstream ( const char * filename, ios_base::openmode mode = ios_base::in ); 

No arroja una excepción, pero tiene una función is_open:

 bool is_open ( ); 

Un constructor bien puede abrir un archivo (no necesariamente una mala idea) y puede lanzar si falla el archivo abierto, o si el archivo de entrada no contiene datos compatibles.

Es un comportamiento razonable para un constructor arrojar una excepción, sin embargo, será limitado en cuanto a su uso.

  • No podrá crear instancias estáticas (nivel de archivo de unidad de comstackción) de esta clase que estén construidas antes de “main ()”, ya que un constructor solo debe lanzarse en el flujo normal.

  • Esto puede extenderse a una evaluación diferida posterior “por primera vez”, donde algo se carga la primera vez que se requiere, por ejemplo, en una construcción boost :: una vez que la función call_once nunca debería arrojarse.

  • Puede usarlo en un entorno IOC (Inversion of Control / Dependency Injection). Esta es la razón por la cual los entornos COI son ventajosos.

  • Asegúrese de que si su constructor tira, no se invocará su destructor. Entonces, todo lo que haya inicializado en el constructor antes de este punto debe estar contenido en un objeto RAII.

  • Más peligroso por cierto puede ser cerrar el archivo en el destructor si esto vacía el buffer de escritura. No hay manera de manejar cualquier error que pueda ocurrir en este momento de manera adecuada.

Puede manejarlo sin excepción dejando el objeto en un estado “fallido”. Esta es la forma en que debe hacerlo en los casos en que no se permite tirar, pero, por supuesto, su código debe verificar el error.