guidelines c++ c++11 c++14 raii c++-faq

c++ - guidelines - Uso de RAII para administrar recursos desde una API de estilo C



c++ core guidelines (2)

La Adquisición de Recursos es Inicialización (RAII) se usa comúnmente en C ++ para administrar la vida útil de los recursos que requieren algún tipo de código de limpieza al final de su vida útil, desde la delete de new punteros de edición hasta la liberación de identificadores de archivos.

¿Cómo uso rápida y fácilmente RAII para administrar la vida útil de un recurso que obtengo de una API de estilo C?

En mi caso, quiero usar RAII para ejecutar automáticamente una función de limpieza desde una API de estilo C cuando la variable que contiene el recurso de estilo C que libera está fuera del alcance. Realmente no necesito un ajuste de recursos adicional más allá de eso, y me gustaría minimizar la sobrecarga de código del uso de RAII aquí. ¿Existe una forma sencilla de utilizar RAII para administrar recursos desde una API de estilo C?

¿Cómo encapsular api de C en clases RAII C ++? está relacionado, pero no creo que sea un duplicado: esa pregunta tiene que ver con una encapsulación más completa, mientras que esta pregunta tiene que ver con el código mínimo para obtener los beneficios de RAII.


Hay una forma fácil de usar RAII para administrar recursos desde una interfaz de estilo C: los punteros inteligentes de la biblioteca estándar, que vienen en dos tipos: std::unique_ptr para recursos con un solo propietario y el equipo de std::shared_ptr y std::weak_ptr para recursos compartidos. Si tiene problemas para decidir cuál es su recurso, estas preguntas y respuestas deben ayudarlo a decidir . Acceder al puntero en bruto que maneja un puntero inteligente es tan fácil como llamar a su función de get miembro.

Si desea una administración de recursos sencilla y basada en el alcance, std::unique_ptr es una excelente herramienta para el trabajo. Está diseñado para una sobrecarga mínima y es fácil de configurar para usar la lógica de destrucción personalizada. Tan fácil, de hecho, que puede hacerlo cuando declara la variable de recurso:

#include <memory> // allow use of smart pointers struct CStyleResource; // c-style resource // resource lifetime management functions CStyleResource* acquireResource(const char *, char*, int); void releaseResource(CStyleResource* resource); // my code: std::unique_ptr<CStyleResource, decltype(&releaseResource)> resource{acquireResource("name", nullptr, 0), releaseResource};

acquireResource ejecuta donde lo llamas, al inicio de la vida útil de la variable. releaseResource se ejecutará al final de la vida útil de la variable, generalmente cuando está fuera de alcance. 1 ¿No me crees? Puede verlo en acción en Coliru , donde he proporcionado algunas implementaciones ficticias para las funciones de adquisición y liberación para que pueda ver cómo sucede.

Puede hacer lo mismo con std::shared_ptr , si necesita esa marca de tiempo de vida del recurso:

// my code: std::shared_ptr<CStyleResource> resource{acquireResource("name", nullptr, 0), releaseResource};

Ahora, ambos están bien, pero la biblioteca estándar tiene std::make_unique 2 y std::make_shared y una de las razones es una excepción más a la seguridad.

GotW # 56 menciona que la evaluación de los argumentos de una función no está ordenada, lo que significa que si tiene una función que toma su nuevo y brillante tipo std::unique_ptr y algún recurso que podría std::unique_ptr en la construcción, suministre ese recurso a una llamada de función como esta:

func( std::unique_ptr<CStyleResource, decltype(&releaseResource)>{ acquireResource("name", nullptr, 0), releaseResource}, ThrowsOnConstruction{});

significa que las instrucciones pueden ser ordenadas así:

  1. llamar a acquireResource
  2. construye ThrowsOnConstruction
  3. construir std::unique_ptr desde el puntero del recurso

y que nuestro precioso recurso de interfaz C no se limpiará correctamente si se realiza el paso 2.

De nuevo, como se mencionó en GotW # 56, en realidad hay una manera relativamente simple de lidiar con el problema de seguridad excepcional. A diferencia de las evaluaciones de expresión en los argumentos de función, las evaluaciones de función no se pueden intercalar. Entonces, si adquirimos un recurso y lo entregamos a unique_ptr dentro de una función, tendremos la garantía de que no sucederá ningún problema para filtrar nuestro recurso cuando ThrowsOnConstruction lance en la construcción. No podemos usar std::make_unique , porque devuelve std::unique_ptr con un eliminador predeterminado, y queremos nuestro propio sabor personalizado de eliminador. También queremos especificar nuestra función de adquisición de recursos, ya que no se puede deducir del tipo sin un código adicional. Implementar tal cosa es bastante simple con el poder de las plantillas: 3

#include <memory> // smart pointers #include <utility> // std::forward template < typename T, typename Deletion, typename Acquisition, typename...Args> std::unique_ptr<T, Deletion> make_c_handler( Acquisition acquisition, Deletion deletion, Args&&...args){ return {acquisition(std::forward<Args>(args)...), deletion}; }

Vivir en coliru

Puedes usarlo así:

auto resource = make_c_handler<CStyleResource>( acquireResource, releaseResource, "name", nullptr, 0);

y llamar a la func preocupaciones, como esto:

func( make_c_handler<CStyleResource( acquireResource, releaseResource, "name", nullptr, 0), ThrowsOnConstruction{});

El compilador no puede tomar la construcción de ThrowsOnConstruction y pegarla entre la llamada para acquireResource y la construcción de unique_ptr , por lo que está bien.

El equivalente de shared_ptr es igualmente simple: simplemente intercambie el valor de retorno de std::unique_ptr<T, Deletion> con std::shared_ptr<T> , y cambie el nombre para indicar un recurso compartido: 4

template < typename T, typename Deletion, typename Acquisition, typename...Args> std::shared_ptr<T> make_c_shared_handler( Acquisition acquisition, Deletion deletion, Args&&...args){ return {acquisition(std::forward<Args>(args)...), deletion}; }

El uso es una vez más similar a la versión unique_ptr :

auto resource = make_c_shared_handler<CStyleResource>( acquireResource, releaseResource, "name", nullptr, 0);

y

func( make_c_shared_handler<CStyleResource( acquireResource, releaseResource, "name", nullptr, 0), ThrowsOnConstruction{});

Editar:

Como se mencionó en los comentarios, hay una mejora adicional que puede hacer al uso de std::unique_ptr : especificar el mecanismo de eliminación en el momento de la compilación, por lo que unique_ptr no necesita llevar un indicador de función al eliminador cuando se mueve alrededor del programa . Hacer un borrado sin estado con una plantilla en el puntero de función que está utilizando requiere cuatro líneas de código, antes de make_c_handler :

template <typename T, void (*Func)(T*)> struct CDeleter{ void operator()(T* t){Func(t);} };

Entonces puedes modificar make_c_handler así:

template < typename T, void (*Deleter)(T*), typename Acquisition, typename...Args> std::unique_ptr<T, CDeleter<T, Deleter>> make_c_handler( Acquisition acquisition, Args&&...args){ return {acquisition(std::forward<Args>(args)...), {}}; }

La sintaxis de uso entonces cambia ligeramente, para

auto resource = make_c_handler<CStyleResource, releaseResource>( acquireResource, "name", nullptr, 0);

Vivir en coliru

make_c_shared_handler no se beneficiaría de cambiar a un eliminador de plantilla, ya que shared_ptr no lleva la información de eliminador disponible en el momento de la compilación.

1. Si el valor del puntero inteligente es nullptr cuando se destruye, no llamará a la función asociada, lo cual es bastante bueno para las bibliotecas que manejan las llamadas de liberación de recursos con punteros nulos como condiciones de error, como SDL.
2. std::make_unique solo se incluyó en la biblioteca en C ++ 14, por lo tanto, si está utilizando C ++ 11, es posible que desee implementar su propia cuenta; es muy útil incluso si no es lo que desea aquí.
3. Esto (y la implementación std::make_unique enlazada en 2) dependen de las plantillas variadic . Si está utilizando VS2012 o VS2010, que tienen una compatibilidad limitada con C ++ 11, no tiene acceso a las plantillas variadic. La implementación de std::make_shared en esas versiones se realizó con sobrecargas individuales para cada número de argumento y combinación de especialización . Haz de eso lo que quieras.
4. std::make_shared realidad tiene una maquinaria más compleja que esta , pero requiere realmente saber qué tan grande será un objeto del tipo. No tenemos esa garantía ya que estamos trabajando con una interfaz de estilo C y es posible que solo tengamos una declaración de reenvío de nuestro tipo de recurso, por lo que no nos preocuparemos por eso aquí.


Un mecanismo de protección de alcance dedicado puede administrar de forma limpia y concisa los recursos de estilo C Como es un concepto relativamente antiguo, hay un número flotando alrededor, pero los guardias de alcance que permiten la ejecución de código arbitrario son por naturaleza los más flexibles. Dos de las bibliotecas populares son SCOPE_EXIT , de la folly biblioteca de código abierto de Facebook (analizada SCOPE_EXIT ), y BOOST_SCOPE_EXIT de (de manera sorprendente) Boost.ScopeExit .

SCOPE_EXIT de SCOPE_EXIT forma parte de una tríada de funcionalidad de flujo de control declarativo que se proporciona en <folly/ScopeGuard.hpp> . SCOPE_EXIT SCOPE_FAIL y SCOPE_SUCCESS ejecutan el código respectivamente cuando el flujo de control sale del ámbito de SCOPE_SUCCESS , cuando sale del ámbito de cierre al lanzar una excepción y cuando sale sin lanzar una excepción. 1

Si tiene una interfaz de estilo C con un recurso y funciones de administración de por vida como esta:

struct CStyleResource; // c-style resource // resource lifetime management functions CStyleResource* acquireResource(const char *, char*, int); void releaseResource(CStyleResource* resource);

Puedes usar SCOPE_EXIT manera:

#include <folly/ScopeGuard.hpp> // my code: auto resource = acquireResource(const char *, char *, int); SCOPE_EXIT{releaseResource(resource);}

Boost.ScopeExit tiene una sintaxis ligeramente diferente. 2 Para hacer lo mismo que el código anterior:

#include <boost/scope_exit.hpp> // my code auto resource = acquireResource(const char *, char *, int); BOOST_SCOPE_EXIT(&resource) { // capture resource by reference releaseResource(resource); } BOOST_SCOPE_EXIT_END

Es posible que en ambos casos le resulte adecuado declarar resource como const , para asegurarse de no cambiar inadvertidamente el valor durante el resto de la función y volver a complicar las preocupaciones de administración de por vida que está tratando de simplificar.

En ambos casos, se releaseResource a releaseResource cuando el flujo de control salga del ámbito de releaseResource , por excepción o no. Tenga en cuenta que también se llamará independientemente de si el resource es nullptr al final del alcance, por lo tanto, si la API requiere funciones de limpieza, no se llamará a los punteros nulos, deberá verificar esa condición usted mismo.

La simplicidad aquí versus el uso de un puntero inteligente tiene el costo de no poder mover su mecanismo de administración de por vida alrededor del programa adjunto tan fácilmente como puede con punteros inteligentes, pero si desea una garantía simple de ejecución de limpieza cuando Las salidas del alcance actual, las guardas del alcance son más que adecuadas para el trabajo.

1. La ejecución del código solo en caso de éxito o fracaso ofrece una funcionalidad de compromiso / retrotracción que puede ser increíblemente útil para la seguridad excepcional y la claridad del código cuando pueden ocurrir múltiples puntos de falla en una sola función, lo que parece ser el motivo principal de la presencia de SCOPE_SUCCESS SCOPE_FAIL , pero estás aquí porque estás interesado en una limpieza incondicional.
2. Como nota al margen, Boost.ScopeExit tampoco tiene una funcionalidad incorporada de éxito / falla como la locura. En la documentación, la funcionalidad de éxito / fracaso, como la que proporciona el protector de alcance de la locura, se implementa al verificar un indicador de éxito que se ha capturado por referencia. La bandera se establece en false al comienzo del alcance, y se establece en true una vez que las operaciones relevantes tengan éxito.