dev - exception c++




Cómo manejar constructores que deben adquirir múltiples recursos de una manera segura y excepcional (2)

¿Hay alguna manera mejor ?

C ++ 11 ofrece una nueva característica llamada delegar constructores que se ocupa de esta situación con mucha gracia. Pero es un poco sutil.

El problema con lanzar excepciones en los constructores es darse cuenta de que el destructor del objeto que estás construyendo no se ejecuta hasta que el constructor está completo. Aunque los destructores de subobjetos (bases y miembros) se ejecutarán si se lanza una excepción, tan pronto como esos subobjetos estén completamente construidos.

La clave aquí es construir X completamente antes de comenzar a agregar recursos a ella, y luego agregar recursos uno a la vez , manteniendo la X en un estado válido a medida que agrega cada recurso. Una vez que la X esté completamente construida, ~X() limpiará cualquier desorden al agregar recursos. Antes de C ++ 11 esto podría parecer:

X x;  // no resources
x.push_back(A(1));  // add a resource
x.push_back(A(2));  // add a resource
// ...

Pero en C ++ 11 puede escribir el constructor de adquisición de múltiples recursos de la siguiente manera:

X(const A& x, const A& y)
    : X{}
{
    data_ = static_cast<A*>(::operator new (2*sizeof(A)));
    ::new(data_) A{x};
    ++size_;
    ::new(data_ + 1) A{y};
    ++size_;
}

Esto es muy parecido a escribir código completamente ignorante de seguridad de excepción. La diferencia es esta línea:

    : X{}

Esto dice: Constrúyeme una X defecto. Después de esta construcción, *this se construye completamente y, si se lanza una excepción en las operaciones posteriores, se ejecuta ~X() . ¡Esto es revolucionario!

Tenga en cuenta que en este caso, una X construida por defecto no adquiere recursos. De hecho, incluso es implícitamente una noexcept . Así que esa parte no va a tirar. Y establece *this en una X válida que contiene una matriz de tamaño 0. ~X() sabe cómo lidiar con ese estado.

Ahora agregue el recurso de la memoria no inicializada. Si eso se produce, todavía tienes una X construida de forma predeterminada y ~X() se ocupa de eso correctamente sin hacer nada.

Ahora agregue el segundo recurso: Una copia construida de x . Si eso ocurre, ~X() seguirá desasignando el data_ buffer, pero sin ejecutar ningún ~A() .

Si el segundo recurso tiene éxito, establezca la X en un estado válido incrementando size_ que es una operación sin noexcept . Si algo después de estos lanzamientos, ~X() limpiará correctamente un búfer de longitud 1.

Ahora pruebe el tercer recurso: Una copia construida de y . Si esa construcción se lanza, ~X() limpiará correctamente tu búfer de longitud 1. Si no se lanza, informa *this que ahora posee un búfer de longitud 2.

El uso de esta técnica no requiere que X sea ​​constructible por defecto. Por ejemplo, el constructor predeterminado podría ser privado. O podría usar algún otro constructor privado que ponga a X en un estado sin recursos:

: X{moved_from_tag{}}

En C ++ 11, generalmente es una buena idea si tu X puede tener un estado sin recursos, ya que esto te permite tener un constructor de movimientos no- noexcept que viene con todo tipo de bondades (y es el tema de una publicación diferente).

C ++ 11 delegar constructores es una técnica muy buena (escalable) para escribir constructores de excepción segura siempre que tenga un estado sin recursos para construir al principio (por ejemplo, un constructor predeterminado noexcept).

Sí, hay formas de hacerlo en C ++ 98/03, pero no son tan bonitas. Debe crear una clase base de X de implementación que contenga la lógica de destrucción de X , pero no la lógica de construcción. He estado allí, hecho eso, me encanta delegar constructores.

Tengo un tipo no trivial que posee múltiples recursos. ¿Cómo lo construyo de una manera segura?

Por ejemplo, aquí hay una clase de demostración X que contiene una matriz de A :

#include "A.h"

class X
{
    unsigned size_ = 0;
    A* data_ = nullptr;

public:
    ~X()
    {
        for (auto p = data_; p < data_ + size_; ++p)
            p->~A();
        ::operator delete(data_);
    }

    X() = default;
    // ...
};

Ahora la respuesta obvia para esta clase en particular es usar std::vector<A> . Y ese es un buen consejo. Pero X es solo un complemento para escenarios más complicados donde X debe poseer múltiples recursos y no es conveniente usar el buen consejo de "use std :: lib". He elegido comunicar la pregunta con esta estructura de datos simplemente porque es familiar.

Para que quede claro: si puede diseñar su X manera que ~X() predeterminado limpie todo ("la regla de cero"), o si ~X() solo libere un único recurso, entonces eso es lo mejor. . Sin embargo, hay momentos en la vida real en que ~X() tiene que lidiar con múltiples recursos, y esta pregunta aborda esas circunstancias.

Así que este tipo ya tiene un buen destructor y un buen constructor predeterminado. Mi pregunta se centra en un constructor no trivial que toma dos A , asigna espacio para ellos y los construye:

X::X(const A& x, const A& y)
    : size_{2}
    , data_{static_cast<A*>(::operator new (size_*sizeof(A)))}
{
    ::new(data_) A{x};
    ::new(data_ + 1) A{y};
}

Tengo una clase de prueba A totalmente instrumentada y si no se lanzan excepciones desde este constructor, funciona perfectamente bien. Por ejemplo, con este controlador de prueba:

int
main()
{
    A a1{1}, a2{2};
    try
    {
        std::cout << "Begin\n";
        X x{a1, a2};
        std::cout << "End\n";
    }
    catch (...)
    {
        std::cout << "Exceptional End\n";
    }
}

La salida es:

A(int state): 1
A(int state): 2
Begin
A(A const& a): 1
A(A const& a): 2
End
~A(1)
~A(2)
~A(2)
~A(1)

Tengo 4 construcciones y 4 destrucciones, y cada destrucción tiene un constructor coincidente. Todo está bien.

Sin embargo, si el constructor de copia de A{2} lanza una excepción, obtendré esta salida:

A(int state): 1
A(int state): 2
Begin
A(A const& a): 1
Exceptional End
~A(2)
~A(1)

Ahora tengo 3 construcciones pero solo 2 destrucciones. La A resultante de A(A const& a): 1 se ha filtrado.

Una forma de resolver este problema es atar al constructor con try/catch . Sin embargo, este enfoque no es escalable. Después de cada asignación de recursos, necesito otra try/catch anidadas para probar la siguiente asignación de recursos y desasignar lo que ya se ha asignado. Tiene la nariz:

X(const A& x, const A& y)
    : size_{2}
    , data_{static_cast<A*>(::operator new (size_*sizeof(A)))}
{
    try
    {
        ::new(data_) A{x};
        try
        {
            ::new(data_ + 1) A{y};
        }
        catch (...)
        {
            data_->~A();
            throw;
        }
    }
    catch (...)
    {
        ::operator delete(data_);
        throw;
    }
}

Esto produce correctamente:

A(int state): 1
A(int state): 2
Begin
A(A const& a): 1
~A(1)
Exceptional End
~A(2)
~A(1)

¡Pero esto es feo! ¿Qué pasa si hay 4 recursos? O 400 ?! ¿Qué pasa si no se conoce el número de recursos en el momento de la compilación?

¿Hay alguna manera mejor ?


Creo que el problema se deriva de una violación del Principio de Responsabilidad Única: la Clase X tiene que lidiar con la gestión de la vida útil de múltiples objetos (y probablemente esa no sea su responsabilidad principal).

El destructor de una clase solo debe liberar los recursos que la clase ha adquirido directamente. Si la clase es solo un compuesto (es decir, una instancia de la clase posee instancias de otras clases), lo ideal es que dependa de la gestión automática de la memoria (a través de RAII) y solo use el destructor predeterminado. Si la clase tiene que administrar algunos recursos especializados manualmente (por ejemplo, abre un descriptor de archivo o una conexión, adquiere un bloqueo o asigna memoria), recomendaría eliminar la responsabilidad de administrar esos recursos a una clase dedicada para este propósito y luego usar instancias de esa clase como miembros.

El uso de la biblioteca de plantillas estándar ayudaría de hecho porque contiene estructuras de datos (como los punteros inteligentes y std::vector<T> ) que manejan exclusivamente este problema. También son compositivos, por lo que incluso si su X tiene que contener múltiples instancias de objetos con estrategias de adquisición de recursos complicadas, el problema de la administración de recursos de una manera segura se resuelve tanto para cada miembro como para la clase compuesta X que lo contiene.





delegating-constructor