En la práctica con C ++, ¿qué es RAII , qué son los punteros inteligentes , cómo se implementan en un programa y cuáles son los beneficios de usar RAII con punteros inteligentes?
fuente
En la práctica con C ++, ¿qué es RAII , qué son los punteros inteligentes , cómo se implementan en un programa y cuáles son los beneficios de usar RAII con punteros inteligentes?
Un ejemplo simple (y quizás sobreutilizado) de RAII es una clase de archivo. Sin RAII, el código podría verse así:
File file("/path/to/file");
// Do stuff with file
file.close();
En otras palabras, debemos asegurarnos de cerrar el archivo una vez que hayamos terminado con él. Esto tiene dos inconvenientes: en primer lugar, donde sea que usemos File, tendremos que llamar a File :: close (); si olvidamos hacer esto, mantendremos el archivo por más tiempo del necesario. El segundo problema es ¿qué pasa si se lanza una excepción antes de cerrar el archivo?
Java resuelve el segundo problema usando una cláusula finalmente:
try {
File file = new File("/path/to/file");
// Do stuff with file
} finally {
file.close();
}
o desde Java 7, una declaración de prueba con recursos:
try (File file = new File("/path/to/file")) {
// Do stuff with file
}
C ++ resuelve ambos problemas usando RAII, es decir, cerrando el archivo en el destructor de Archivo. Siempre y cuando el objeto File se destruya en el momento correcto (que debería ser de todos modos), nos encargamos de cerrar el archivo. Entonces, nuestro código ahora se parece a:
File file("/path/to/file");
// Do stuff with file
// No need to close it - destructor will do that for us
Esto no se puede hacer en Java ya que no hay garantía de cuándo se destruirá el objeto, por lo que no podemos garantizar cuándo se liberará un recurso como un archivo.
En punteros inteligentes: muchas veces, solo creamos objetos en la pila. Por ejemplo (y robando un ejemplo de otra respuesta):
void foo() {
std::string str;
// Do cool things to or using str
}
Esto funciona bien, pero ¿qué pasa si queremos devolver str? Podríamos escribir esto:
std::string foo() {
std::string str;
// Do cool things to or using str
return str;
}
Entonces, ¿qué hay de malo en eso? Bueno, el tipo de retorno es std :: string, por lo que significa que estamos regresando por valor. Esto significa que copiamos str y en realidad devolvemos la copia. Esto puede ser costoso y es posible que deseemos evitar el costo de copiarlo. Por lo tanto, podríamos tener la idea de regresar por referencia o por puntero.
std::string* foo() {
std::string str;
// Do cool things to or using str
return &str;
}
Lamentablemente, este código no funciona. Estamos devolviendo un puntero a str, pero str se creó en la pila, por lo que se eliminará una vez que salgamos de foo (). En otras palabras, cuando la persona que llama obtiene el puntero, es inútil (y posiblemente peor que inútil ya que usarlo podría causar todo tipo de errores funky)
Entonces, ¿cuál es la solución? Podríamos crear str en el montón usando new, de esa manera, cuando se complete foo (), str no será destruido.
std::string* foo() {
std::string* str = new std::string();
// Do cool things to or using str
return str;
}
Por supuesto, esta solución tampoco es perfecta. La razón es que hemos creado str, pero nunca lo eliminamos. Esto podría no ser un problema en un programa muy pequeño, pero en general, queremos asegurarnos de eliminarlo. Podríamos decir que la persona que llama debe eliminar el objeto una vez que haya terminado con él. La desventaja es que la persona que llama tiene que administrar la memoria, lo que agrega complejidad adicional y podría equivocarse, lo que lleva a una pérdida de memoria, es decir, no elimina el objeto a pesar de que ya no es necesario.
Aquí es donde entran los punteros inteligentes. El siguiente ejemplo usa shared_ptr: le sugiero que mire los diferentes tipos de punteros inteligentes para aprender lo que realmente quiere usar.
shared_ptr<std::string> foo() {
shared_ptr<std::string> str = new std::string();
// Do cool things to or using str
return str;
}
Ahora, shared_ptr contará el número de referencias a str. Por ejemplo
shared_ptr<std::string> str = foo();
shared_ptr<std::string> str2 = str;
Ahora hay dos referencias a la misma cadena. Una vez que no haya referencias restantes a str, se eliminará. Como tal, ya no tiene que preocuparse por eliminarlo usted mismo.
Edición rápida: como algunos de los comentarios han señalado, este ejemplo no es perfecto por (¡al menos!) Dos razones. En primer lugar, debido a la implementación de cadenas, copiar una cadena tiende a ser económico. En segundo lugar, debido a lo que se conoce como optimización del valor de retorno denominado, el retorno por valor puede no ser costoso ya que el compilador puede hacer algo de inteligencia para acelerar las cosas.
Entonces, intentemos un ejemplo diferente usando nuestra clase File.
Digamos que queremos usar un archivo como registro. Esto significa que queremos abrir nuestro archivo en modo de solo agregar:
File file("/path/to/file", File::append);
// The exact semantics of this aren't really important,
// just that we've got a file to be used as a log
Ahora, configuremos nuestro archivo como el registro de un par de otros objetos:
void setLog(const Foo & foo, const Bar & bar) {
File file("/path/to/file", File::append);
foo.setLogFile(file);
bar.setLogFile(file);
}
Desafortunadamente, este ejemplo termina horriblemente: el archivo se cerrará tan pronto como termine este método, lo que significa que foo y bar ahora tienen un archivo de registro no válido. Podríamos construir un archivo en el montón, y pasar un puntero a un archivo para foo y bar:
void setLog(const Foo & foo, const Bar & bar) {
File* file = new File("/path/to/file", File::append);
foo.setLogFile(file);
bar.setLogFile(file);
}
Pero entonces, ¿quién es responsable de eliminar el archivo? Si ninguno de los dos elimina el archivo, tenemos una pérdida de memoria y de recursos. No sabemos si foo o bar terminarán primero con el archivo, por lo que tampoco podemos esperar que el archivo se elimine ellos mismos. Por ejemplo, si foo elimina el archivo antes de que la barra haya terminado con él, la barra ahora tiene un puntero no válido.
Entonces, como habrás adivinado, podríamos usar punteros inteligentes para ayudarnos.
void setLog(const Foo & foo, const Bar & bar) {
shared_ptr<File> file = new File("/path/to/file", File::append);
foo.setLogFile(file);
bar.setLogFile(file);
}
Ahora, nadie debe preocuparse por eliminar el archivo: una vez que foo y bar hayan terminado y ya no tengan ninguna referencia al archivo (probablemente debido a la destrucción de foo y bar), el archivo se eliminará automáticamente.
RAII Este es un nombre extraño para un concepto simple pero impresionante. Mejor es el nombre de Scope Bound Resource Management (SBRM). La idea es que a menudo se asignan recursos al comienzo de un bloque y es necesario liberarlo a la salida de un bloque. La salida del bloque puede ocurrir mediante el control de flujo normal, saltando fuera de él e incluso por una excepción. Para cubrir todos estos casos, el código se vuelve más complicado y redundante.
Solo un ejemplo haciéndolo sin SBRM:
Como puede ver, hay muchas maneras en que podemos ser criticados. La idea es que encapsulemos la gestión de recursos en una clase. La inicialización de su objeto adquiere el recurso ("Adquisición de recursos es inicialización"). En el momento en que salimos del bloque (alcance del bloque), el recurso se libera nuevamente.
Eso es bueno si tienes clases propias que no son únicamente con el propósito de asignar / desasignar recursos. La asignación sería una preocupación adicional para hacer su trabajo. Pero tan pronto como solo desee asignar / desasignar recursos, lo anterior se vuelve desagradable. Debe escribir una clase de ajuste para cada tipo de recurso que adquiera. Para facilitar eso, los punteros inteligentes le permiten automatizar ese proceso:
Normalmente, los punteros inteligentes son envoltorios finos alrededor de nuevo / eliminar que simplemente llaman
delete
cuando el recurso que poseen se sale del alcance. Algunos punteros inteligentes, como shared_ptr, le permiten decirles un llamado eliminador, que se usa en lugar dedelete
. Eso le permite, por ejemplo, administrar identificadores de ventana, recursos de expresión regular y otras cosas arbitrarias, siempre que le informe a shared_ptr sobre el eliminador correcto.Hay diferentes punteros inteligentes para diferentes propósitos:
unique_ptr
es un puntero inteligente que posee un objeto exclusivamente. No está en aumento, pero probablemente aparecerá en el próximo estándar de C ++. No se puede copiar, pero admite la transferencia de propiedad . Algún código de ejemplo (siguiente C ++):Código:
A diferencia de auto_ptr, unique_ptr se puede poner en un contenedor, porque los contenedores podrán contener tipos no copiables (pero móviles), como streams y unique_ptr también.
scoped_ptr
es un puntero inteligente de impulso que no se puede copiar ni mover. Es lo perfecto para usar cuando quieres asegurarte de que los punteros se eliminen cuando estén fuera de alcance.Código:
shared_ptr
es de propiedad compartida. Por lo tanto, es copiable y móvil. Varias instancias de puntero inteligente pueden poseer el mismo recurso. Tan pronto como el último puntero inteligente que posee el recurso se salga del alcance, el recurso se liberará. Algunos ejemplos del mundo real de uno de mis proyectos:Código:
Como puede ver, la fuente de la trama (función fx) se comparte, pero cada una tiene una entrada separada, en la que establecemos el color. Hay una clase weak_ptr que se usa cuando el código necesita referirse al recurso que posee un puntero inteligente, pero no necesita ser dueño del recurso. En lugar de pasar un puntero sin formato, debe crear un débil_ptr. Lanzará una excepción cuando advierta que intenta acceder al recurso por una ruta de acceso débil_ptr, a pesar de que ya no existe shared_ptr que sea el propietario del recurso.
fuente
unique_ptr
, ysort
también se cambiarán de la misma manera.La premisa y las razones son simples, en concepto.
RAII es el paradigma de diseño para garantizar que las variables manejen toda la inicialización necesaria en sus constructores y toda la limpieza necesaria en sus destructores. Esto reduce toda la inicialización y limpieza a un solo paso.
C ++ no requiere RAII, pero se acepta cada vez más que el uso de métodos RAII producirá un código más robusto.
La razón por la que RAII es útil en C ++ es que C ++ administra intrínsecamente la creación y destrucción de variables a medida que entran y salen del alcance, ya sea a través del flujo de código normal o mediante el desenrollado de la pila desencadenado por una excepción. Eso es un regalo de promoción en C ++.
Al vincular toda la inicialización y limpieza a estos mecanismos, se asegura de que C ++ también se encargará de este trabajo por usted.
Hablar sobre RAII en C ++ generalmente conduce a la discusión de los punteros inteligentes, porque los punteros son particularmente frágiles cuando se trata de la limpieza. Al administrar la memoria asignada en el montón adquirida de malloc o nueva, generalmente es responsabilidad del programador liberar o eliminar esa memoria antes de que se destruya el puntero. Los punteros inteligentes usarán la filosofía RAII para garantizar que los objetos asignados al montón se destruyan cada vez que se destruya la variable del puntero.
fuente
El puntero inteligente es una variación de RAII. RAII significa que la adquisición de recursos es la inicialización. El puntero inteligente adquiere un recurso (memoria) antes del uso y luego lo arroja automáticamente en un destructor. Suceden dos cosas:
Por ejemplo, otro ejemplo es el socket de red RAII. En este caso:
Ahora, como puede ver, RAII es una herramienta muy útil en la mayoría de los casos, ya que ayuda a las personas a acostarse.
Las fuentes de C ++ de punteros inteligentes están en millones alrededor de la red, incluidas las respuestas por encima de mí.
fuente
Boost tiene varios de estos, incluidos los de Boost. Interproceso para memoria compartida. Simplifica enormemente la gestión de la memoria, especialmente en situaciones que provocan dolor de cabeza, como cuando tienes 5 procesos que comparten la misma estructura de datos: cuando todos terminan con un trozo de memoria, quieres que se libere automáticamente y no tengas que sentarte allí tratando de descubrir quién debería ser responsable de invocar
delete
un trozo de memoria, para que no termine con una pérdida de memoria o un puntero que se libera por error dos veces y puede corromper todo el montón.fuente
Pase lo que pase, la barra se eliminará correctamente una vez que se haya dejado atrás el alcance de la función foo ().
Internamente, las implementaciones de std :: string a menudo usan punteros contados por referencia. Por lo tanto, la cadena interna solo necesita copiarse cuando una de las copias de las cadenas cambió. Por lo tanto, un puntero inteligente de recuento de referencia permite copiar solo algo cuando sea necesario.
Además, el recuento de referencias internas hace posible que la memoria se elimine correctamente cuando ya no se necesita la copia de la cadena interna.
fuente