RAII y punteros inteligentes en C ++

Respuestas:

317

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.

Michael Williamson
fuente
77
Cabe señalar que muchas implementaciones de cadenas se implementan en términos de un puntero contado de referencia. Estas semánticas de copia en escritura hacen que devolver una cadena por valor sea realmente económico.
77
Incluso para los que no lo son, muchos compiladores implementan la optimización NRV que se encargaría de los gastos generales. En general, creo que shared_ptr rara vez es útil, solo quédese con RAII y evite la propiedad compartida.
Nemanja Trifunovic
27
devolver una cadena no es una buena razón para usar punteros inteligentes realmente. la optimización del valor de retorno puede optimizar fácilmente el retorno, y la semántica de movimiento de c ++ 1x eliminará por completo una copia (cuando se usa correctamente). Muestre algún ejemplo del mundo real (por ejemplo, cuando compartimos el mismo recurso) en su lugar :)
Johannes Schaub - litb
1
Creo que su conclusión inicial sobre por qué Java no puede hacer esto carece de claridad. La forma más fácil de describir esta limitación en Java o C # es porque no hay forma de asignar en la pila. C # permite la asignación de la pila a través de una palabra clave especial, sin embargo, pierde seguridad de tipo.
ApplePieIsGood
44
@Nemanja Trifunovic: ¿Por RAII en este contexto te refieres a devolver copias / crear objetos en la pila? Eso no funciona si tiene objetos de devolución / aceptación de tipos que se pueden subclasificar. Luego, debe usar un puntero para evitar cortar el objeto, y diría que un puntero inteligente a menudo es mejor que uno sin formato en esos casos.
Frank Osterfeld
141

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:

void o_really() {
     resource * r = allocate_resource();
     try {
         // something, which could throw. ...
     } catch(...) {
         deallocate_resource(r);
         throw;
     }
     if(...) { return; } // oops, forgot to deallocate
     deallocate_resource(r);
}

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.

struct resource_holder {
    resource_holder() {
        r = allocate_resource();
    }
    ~resource_holder() {
        deallocate_resource(r);
    }
    resource * r;
};

void o_really() {
     resource_holder r;
     // something, which could throw. ...
     if(...) { return; }
}

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:

shared_ptr<Entry> create_entry(Parameters p) {
    shared_ptr<Entry> e(Entry::createEntry(p), &Entry::freeEntry);
    return e;
}

Normalmente, los punteros inteligentes son envoltorios finos alrededor de nuevo / eliminar que simplemente llaman deletecuando 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 de delete. 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:

unique_ptr<plot_src> p(new plot_src); // now, p owns
unique_ptr<plot_src> u(move(p)); // now, u owns, p owns nothing.
unique_ptr<plot_src> v(u); // error, trying to copy u

vector<unique_ptr<plot_src>> pv; 
pv.emplace_back(new plot_src); 
pv.emplace_back(new plot_src);

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:

void do_something() {
    scoped_ptr<pipe> sp(new pipe);
    // do something here...
} // when going out of scope, sp will delete the pointer automatically. 

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:

shared_ptr<plot_src> p(new plot_src(&fx));
plot1->add(p)->setColor("#00FF00");
plot2->add(p)->setColor("#FF0000");
// if p now goes out of scope, the src won't be freed, as both plot1 and 
// plot2 both still have references. 

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.

Johannes Schaub - litb
fuente
Hasta donde sé, los objetos que no se pueden copiar no son buenos para usar en contenedores stl, ya que dependen de la semántica de valor: ¿qué sucede si desea clasificar ese contenedor? Ordenar los elementos de copia hace ...
fmuecke
Los contenedores C ++ 0x se cambiarán para que respete los tipos de solo movimiento como unique_ptr, y sorttambién se cambiarán de la misma manera.
Johannes Schaub - litb
¿Recuerdas dónde escuchaste por primera vez el término SBRM? James está tratando de localizarlo.
GManNickG
¿Qué encabezados o bibliotecas debo incluir para usarlos? alguna lectura adicional sobre esto?
atoMerz
Un consejo aquí: si hay una respuesta a una pregunta formulada por C ++ @litb, es la respuesta correcta (independientemente de los votos o la respuesta marcada como "correcta") ...
FNL
32

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.

Drew Dormann
fuente
Además, los punteros son la aplicación más común de RAII; es probable que asigne miles de veces más punteros que cualquier otro recurso.
Eclipse
8

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:

  1. Asignamos memoria antes de usarla, siempre, incluso cuando no tenemos ganas, es difícil hacerlo de otra manera con un puntero inteligente. Si esto no sucediera, intentará acceder a la memoria NULL, lo que provocará un bloqueo (muy doloroso).
  2. Liberamos memoria incluso cuando hay un error. No queda memoria pendiente.

Por ejemplo, otro ejemplo es el socket de red RAII. En este caso:

  1. Abrimos el socket de red antes de usarlo, siempre, incluso cuando no tenemos ganas, es difícil hacerlo de otra manera con RAII. Si intenta hacer esto sin RAII, puede abrir un socket vacío para, por ejemplo, la conexión MSN. Entonces, un mensaje como "hagámoslo esta noche" podría no transferirse, los usuarios no tendrían sexo, y podría arriesgarse a ser despedido.
  2. Cerramos toma de red , incluso cuando hay un error. No se deja ningún socket colgando, ya que esto podría evitar que el mensaje de respuesta "seguro que estará en la parte inferior" golpee al remitente.

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í.

mannicken
fuente
2

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 deleteun 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.

Jason S
fuente
0
vacío foo ()
{
   std :: barra de cadena;
   //
   // más código aquí
   //
}

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
1
nulo f () {Obj x; } Obj x se elimina mediante la creación / destrucción del marco de la pila (desenrollado) ... no está relacionado con el recuento de referencias.
Hernán
El recuento de referencias es una característica de la implementación interna de la cadena. RAII es el concepto detrás de la eliminación de objetos cuando el objeto sale del alcance. La pregunta era sobre RAII y también punteros inteligentes.
1
"No importa lo que pase", ¿qué pasa si se lanza una excepción antes de que regrese la función?
titaniumdecoy
¿Qué función se devuelve? Si se lanza una excepción en foo, se elimina la barra. El constructor predeterminado de la barra que lanza una excepción sería un evento extraordinario.