¿Cómo manejar los casos de falla en el constructor de clase C ++?

21

Tengo una clase CPP cuyo constructor hace algunas operaciones. Algunas de estas operaciones pueden fallar. Sé que los constructores no devuelven nada.

Mis preguntas son

  1. ¿Está permitido hacer algunas operaciones que no sean inicializar miembros en un constructor?

  2. ¿Es posible decirle a la función de llamada que algunas operaciones en el constructor han fallado?

  3. ¿Puedo hacer que new ClassName()return NULL si se producen algunos errores en el constructor?

MayurK
fuente
22
Puede lanzar una excepción desde el constructor. Es un patrón completamente válido.
Andy
1
Probablemente debería echar un vistazo a algunos de los patrones de creación del GoF . Recomiendo el patrón de fábrica.
SpaceTrucker
2
Un ejemplo común de # 1 es la validación de datos. Es decir, si tiene una clase Square, con un constructor que toma un parámetro, la longitud de un lado, desea verificar si ese valor es mayor que 0.
David dice Reinstate Monica
1
Para la primera pregunta, permítame advertirle que las funciones virtuales pueden comportarse de manera poco intuitiva en los constructores. Lo mismo con los deconstructores. Cuidado con llamar a tal.
1
# 3 - ¿Por qué querrías devolver un NULL? Uno de los beneficios de OO NO es tener que verificar los valores de retorno. Simplemente capture () las posibles excepciones apropiadas.
MrWonderful

Respuestas:

42
  1. Sí, aunque algunos estándares de codificación pueden prohibirlo.

  2. Sí. La forma recomendada es lanzar una excepción. Alternativamente, puede almacenar la información de error dentro del objeto y proporcionar métodos para acceder a esta información.

  3. No.

Sebastian Redl
fuente
44
A menos que el objeto todavía esté en estado válido, aunque alguna parte de los argumentos del constructor no cumplió con los requisitos y, por lo tanto, está marcado como un error, 2) realmente no se recomienda hacer. Es mejor cuando un objeto existe en un estado válido o no existe en absoluto.
Andy
@DavidPacker De acuerdo, ver aquí: stackoverflow.com/questions/77639/… Pero algunas pautas de codificación prohíben excepciones, lo cual es problemático para los constructores.
Sebastian Redl
De alguna manera, ya te he dado un voto positivo por esa respuesta, Sebastian. Interesante. : D
Andy
10
@ooxi No, no lo es. Su nueva versión reemplazada se llama para asignar memoria, pero el compilador realiza la llamada al constructor una vez que su operador ha regresado, lo que significa que no puede detectar el error. Eso se supone que se llama nuevo; no es para objetos asignados a la pila, que deberían ser la mayoría de ellos.
Sebastian Redl
1
Para el n. ° 1, RAII es un ejemplo común en el que puede ser necesario hacer más en el constructor.
Eric
20

Puede crear un método estático que realice el cálculo y devuelva un objeto en caso de éxito o no en caso de error.

Dependiendo de cómo se haga esta construcción del objeto, podría ser mejor crear otro objeto que permita la construcción de objetos en un método no estático.

Llamar a un constructor indirectamente a menudo se denomina "fábrica".

Esto también le permitiría devolver un objeto nulo, que podría ser una mejor solución que devolver nulo.

nulo
fuente
Gracias @ null! Desafortunadamente, no puedo aceptar dos respuestas aquí :( ¡De lo contrario, habría aceptado esta respuesta también! ¡Gracias de nuevo!
MayurK
@MayurK no se preocupe, la respuesta aceptada no es marcar la respuesta correcta, sino la que funcionó para usted.
nulo
3
@ null: en C ++, no puedes regresar NULL. Por ejemplo, en int foo() { return NULLrealidad devolvería 0(cero), un objeto entero. En std::string foo() { return NULL; }accidentalmente llamarías std::string::string((const char*)NULL)cuál es Comportamiento indefinido (NULL no apunta a una cadena terminada en \ 0).
MSalters
3
std :: opcional puede estar muy lejos, pero siempre puedes usar boost :: opcional si quieres ir por ese camino.
Sean Burton
1
@Vld: en C ++, los objetos no están restringidos a los tipos de clase. Y con la programación genérica, no es raro terminar con fábricas int. Por ejemplo, std::allocator<int>es una fábrica perfectamente sana.
MSalters
5

@SebastianRedl ya dio las respuestas simples y directas, pero alguna explicación adicional podría ser útil.

TL; DR = hay una regla de estilo para mantener a los constructores simples, hay razones para ello, pero esas razones se relacionan principalmente con un estilo de codificación histórico (o simplemente malo). El manejo de excepciones en los constructores está bien definido, y los destructores aún se llamarán para miembros y variables locales completamente construidas, lo que significa que no debería haber ningún problema en el código idiomático de C ++. La regla de estilo persiste de todos modos, pero normalmente eso no es un problema: no toda la inicialización tiene que estar en el constructor, y particularmente no necesariamente ese constructor.


Es una regla de estilo común que los constructores deben hacer el mínimo absoluto posible para configurar un estado válido definido. Si su inicialización es más compleja, debe manejarse fuera del constructor. Si no hay un valor económico para inicializar que su constructor pueda configurar, debe debilitar a los invasores forzados por su clase para agregar uno. Por ejemplo, si asignar almacenamiento para que su clase lo administre es demasiado costoso, agregue un estado nulo aún no asignado, porque, por supuesto, tener estados de casos especiales como nulo nunca causó problemas a nadie. Ejem.

Aunque es común, ciertamente en esta forma extrema está muy lejos de ser absoluto. En particular, como indica mi sarcasmo, estoy en el campamento que dice que debilitar a los invariantes es casi siempre un precio demasiado alto. Sin embargo, hay razones detrás de la regla de estilo, y hay formas de tener constructores mínimos e invariantes fuertes.

Las razones se relacionan con la limpieza automática del destructor, particularmente frente a excepciones. Básicamente, tiene que haber un punto bien definido cuando el compilador se hace responsable de llamar a los destructores. Mientras todavía está en una llamada de constructor, el objeto no está necesariamente completamente construido, por lo que no es válido llamar al destructor para ese objeto. Por lo tanto, la responsabilidad de destruir el objeto solo se transfiere al compilador cuando el constructor se completa con éxito. Esto se conoce como RAII (asignación de recursos es la inicialización), que no es realmente el mejor nombre.

Si un tiro excepción se produce dentro del constructor, las necesidades de piezas construidas por cualquier cosa que hay que limpiar explícitamente, por lo general en una try .. catch.

Sin embargo, los componentes del objeto que ya se han construido con éxito son responsabilidad del compilador. Esto significa que, en la práctica, no es realmente un gran problema. p.ej

classname (args) : base1 (args), member2 (args), member3 (args)
{
}

El cuerpo de este constructor está vacío. En tanto que los constructores para base1, member2y member3son una excepción segura, no hay nada de qué preocuparse. Por ejemplo, si el constructor de member2tiros, ese constructor es responsable de limpiarse. La base base1ya estaba completamente construida, por lo que su destructor se llamará automáticamente. member3nunca fue construido parcialmente, por lo que no necesita limpieza.

Incluso cuando hay un cuerpo, las variables locales que se construyeron completamente antes de que se lanzara la excepción se destruirán automáticamente, al igual que cualquier otra función. Los cuerpos de constructor que hacen juegos malabares con punteros en bruto, o "poseen" algún tipo de estado implícito (almacenado en otro lugar), lo que generalmente significa que una llamada de función de inicio / adquisición debe coincidir con una llamada de finalización / liberación, puede causar problemas de seguridad excepcionales, pero el problema real allí está fallando en administrar un recurso adecuadamente a través de una clase. Por ejemplo, si reemplaza punteros sin formato con unique_ptren el constructor, unique_ptrse llamará automáticamente al destructor para si es necesario.

Todavía hay otras razones que la gente da para preferir constructores de hacer lo mínimo. Uno es simplemente porque existe la regla de estilo, muchas personas suponen que las llamadas de constructor son baratas. Una forma de tener eso, pero aún tener invariantes fuertes, es tener una clase de fábrica / constructor separada que tenga invariantes debilitados en su lugar, y que establezca el valor inicial necesario usando (potencialmente muchas) llamadas de función miembro normales. Una vez que tenga el estado inicial que necesita, pase ese objeto como argumento al constructor de la clase con los invariantes fuertes. Eso puede "robar las entrañas" del objeto de invariantes débiles, mover la semántica, que es una noexceptoperación barata (y generalmente ).

Y, por supuesto, puede incluir eso en una make_whatever ()función, por lo que las personas que llaman de esa función nunca necesitan ver la instancia de clase de invariantes debilitados.

Steve314
fuente
El párrafo donde escribe "Mientras está en una llamada de constructor, el objeto no está necesariamente completamente construido, por lo que no es válido llamar al destructor para ese objeto. Por lo tanto, la responsabilidad de destruir el objeto solo se transfiere al compilador cuando el constructor se complete con éxito "realmente podría usar una actualización sobre la delegación de constructores. El objeto se construye completamente cuando termina el constructor más derivado, y se llamará al destructor si ocurre una excepción dentro de un constructor delegante.
Ben Voigt
Por lo tanto, el constructor "hacer el mínimo" puede ser privado, y la función "make_whatever ()" puede ser otro constructor que llame al privado.
Ben Voigt
Esta no es la definición de RAII con la que estoy familiarizado. Mi comprensión de RAII es adquirir intencionalmente un recurso en (y solo en) el constructor de un objeto y liberarlo en su destructor. De esta manera, el objeto se puede usar en la pila para gestionar automáticamente la adquisición y liberación de los recursos que encapsula. El ejemplo clásico es un bloqueo que adquiere un mutex cuando se construye y lo libera en la destrucción.
Eric
1
@Eric: Sí, es una práctica absolutamente estándar, una práctica estándar que comúnmente se llama RAII. No soy solo yo quien amplía la definición, es incluso Stroustrup, en algunas conversaciones. Sí, RAII se trata de vincular los ciclos de vida de los recursos con los ciclos de vida de los objetos, siendo el modelo mental la propiedad.
Steve314
1
@Eric: las respuestas anteriores se eliminaron porque se explicaron mal. De todos modos, los objetos en sí mismos son recursos que pueden ser propiedad. Todo debe tener un propietario, en una cadena hasta la mainfunción o variables estáticas / globales. Un objeto asignado usandonew , no es propiedad hasta que asigne esa responsabilidad, pero los punteros inteligentes poseen los objetos asignados en el montón a los que hacen referencia, y los contenedores poseen sus estructuras de datos. Los propietarios pueden optar por eliminar antes, el destructor de propietarios es el responsable final.
Steve314