smart - RAII y punteros inteligentes en C++
smart pointers c++11 (6)
En la práctica con C ++, ¿qué es RAII , qué indicadores inteligentes , cómo se implementan en un programa y cuáles son los beneficios de usar RAII con punteros inteligentes?
La premisa y las razones son simples, en concepto.
RAII es el paradigma de diseño para garantizar que las variables manejen todas las inicializaciones necesarias en sus constructores y toda la limpieza necesaria en sus destructores. Esto reduce toda inicialización y limpieza en un solo paso.
C ++ no requiere RAII, pero cada vez se acepta 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 ingresan y salen del alcance, ya sea a través del flujo de código normal o mediante el desenrollado de la pila provocado por una excepción. Eso es un regalo de promoción en C ++.
Al vincular toda la inicialización y la limpieza a estos mecanismos, se asegura que C ++ también se encargará de este trabajo.
Hablar de RAII en C ++ generalmente lleva a la discusión de los punteros inteligentes, ya que los punteros son particularmente frágiles en lo que respecta a 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 utilizarán la filosofía RAII para garantizar que los objetos asignados en el montón se destruyan cada vez que se destruya la variable del puntero.
Boost tiene varios de ellos, incluidos los de Boost. Boost.Interprocess para memoria compartida. Simplifica enormemente la administració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 todo el mundo termina con un pedazo de memoria, quieres que se libere automáticamente y no te sientas ahí tratando de descubrir ¿Quién debería ser responsable de llamar a delete
en un trozo de memoria, para no acabar con una pérdida de memoria o un puntero que se libera por error dos veces y puede dañar todo el montón?
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 descarta automáticamente en un destructor. Dos cosas pasan:
- Asignamos memoria antes de usarla, siempre, incluso cuando no tenemos ganas, es difícil hacer otra cosa con un puntero inteligente. Si esto no sucediera, intentará acceder a la memoria NULL, lo que provocará un bloqueo (muy doloroso).
- Liberamos memoria incluso cuando hay un error. No queda memoria colgando.
Por ejemplo, otro ejemplo es el conector de red RAII. En este caso:
- Abrimos el conector 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 el zócalo vacío, por ejemplo, la conexión MSN. Entonces, un mensaje como "hagámoslo esta noche" podría no transferirse, los usuarios no se acostarán, y podrías arriesgarte a ser despedido.
- Cerramos el socket de red incluso cuando hay un error. No se deja ningún enchufe colgando, ya que esto podría evitar que el mensaje de respuesta "seguro esté abajo" 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 punteros inteligentes de C ++ están en millones en la red, incluidas las respuestas que están por encima de mí.
Un ejemplo simple (y quizás usado en exceso) de RAII es una clase de archivo. Sin RAII, el código podría verse más o menos así:
File file("/path/to/file");
// Do stuff with file
file.close();
En otras palabras, debemos asegurarnos de cerrar el archivo una vez que lo hayamos terminado. Esto tiene dos inconvenientes: en primer lugar, donde sea que usemos File, tendremos que llamar a File :: close () - si olvidamos hacer esto, estamos reteniendo el archivo por más tiempo de lo que necesitamos. El segundo problema es: ¿qué ocurre si lanzamos una excepción antes de cerrar el archivo?
Java resuelve el segundo problema usando una cláusula finally:
try {
File file = new File("/path/to/file");
// Do stuff with file
} finally {
file.close();
}
C ++ resuelve ambos problemas usando RAII, es decir, cerrando el archivo en el destructor de File. Siempre que el objeto File se destruya en el momento correcto (que debería ser de todos modos), el cierre del archivo se hará por nosotros. Por lo tanto, nuestro código ahora se ve algo así como:
File file("/path/to/file");
// Do stuff with file
// No need to close it - destructor will do that for us
La razón por la que esto no se puede hacer en Java es que no tenemos garantía sobre 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, simplemente 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 ¿y si queremos regresar a str? Podríamos escribir esto:
std::string foo() {
std::string str;
// Do cool things to or using str
return str;
}
Entonces, ¿qué pasa con 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 pensar en 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, para cuando la persona que llama obtiene el puntero, es inútil (y podría decirse que es 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 completa foo (), str no se destruirá.
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 que lo eliminemos. 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 puede equivocarse, dando lugar a una pérdida de memoria, es decir, no eliminando el objeto aunque ya no sea necesario.
Aquí es donde entran los punteros inteligentes. El siguiente ejemplo usa shared_ptr: sugiero que observe los diferentes tipos de punteros inteligentes para saber qué es 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 para (¡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, devolver por valor puede no ser costoso, ya que el compilador puede hacer algo de inteligencia para acelerar las cosas.
Entonces, probemos un ejemplo diferente usando nuestra clase File.
Digamos que queremos usar un archivo como un registro. Esto significa que queremos abrir nuestro archivo en el 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, establezcamos 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);
}
Lamentablemente, este ejemplo finaliza de forma horrible: el archivo se cerrará tan pronto como finalice 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 al archivo tanto para foo como para la barra:
void setLog(const Foo & foo, const Bar & bar) {
File* file = new File("/path/to/file", File::append);
foo.setLogFile(file);
bar.setLogFile(file);
}
Pero, ¿quién es el responsable de eliminar el archivo? Si ninguno de los dos elimina el archivo, entonces tenemos una fuga tanto de memoria como de recursos. No sabemos si foo o bar terminarán primero con el archivo, por lo que no podemos esperar eliminar el archivo ellos mismos. Por ejemplo, si foo elimina el archivo antes de que la barra lo haya terminado, 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 tiene que preocuparse por eliminar el archivo: una vez que tanto foo como la barra hayan finalizado y ya no tengan referencias al archivo (probablemente debido a que foo y bar se han destruido), el archivo se eliminará automáticamente.
RAII Este es un nombre extraño para un concepto simple pero impresionante. Mejor es el nombre Scope Bound Resource Management (SBRM). La idea es que a menudo asigna recursos al comienzo de un bloque y necesita liberarlo a la salida de un bloque. Salir del bloqueo puede ocurrir mediante un control de flujo normal, saltando 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 de hacerlo 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 ven, hay muchas formas en que podemos obtenerlo. 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 tiene clases propias que no son solo con el propósito de asignar / desasignar recursos. La asignación sería solo una preocupación adicional para hacer su trabajo. Pero tan pronto como solo quiera asignar / desasignar recursos, lo anterior se vuelve inseguro. Tienes que escribir una clase de ajuste para cada tipo de recurso que adquieras. Para facilitar eso, los indicadores 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 delgados alrededor de nuevo / eliminar que llaman delete
cuando el recurso que poseen queda fuera 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 ventanas, recursos de expresiones regulares y otras cosas arbitrarias, siempre y cuando le cuente a shared_ptr sobre el eliminador derecho.
Hay diferentes punteros inteligentes para diferentes propósitos:
unique_ptr
es un puntero inteligente que posee un objeto exclusivamente. No está en alza, pero probablemente aparecerá en el próximo estándar de C ++. No se puede copiar, pero admite la transferencia de propiedad . Un 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 colocar en un contenedor, ya que los contenedores podrán contener tipos que no se pueden copiar (pero que se pueden mover), como streams y unique_ptr.
scoped_ptr
es un puntero inteligente de refuerzo que no se puede copiar ni mover. Es lo perfecto para usar cuando desee asegurarse de que los punteros se borren cuando se salga del 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 para propiedad compartida. Por lo tanto, es tanto copiable como móvil. Varias instancias de puntero inteligente pueden ser propietarias del mismo recurso. Tan pronto como el último puntero inteligente que posee el recurso salga del alcance, el recurso será liberado. Un ejemplo 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, el origen del argumento (función fx) se comparte, pero cada uno tiene una entrada separada, en la que establecemos el color. Existe una clase weak_ptr que se usa cuando el código necesita referirse al recurso propiedad de un puntero inteligente, pero no necesita ser propietario del recurso. En lugar de pasar un puntero sin formato, debe crear un weak_ptr. Arrojará una excepción cuando advierta que intenta acceder al recurso mediante una ruta de acceso de acceso débil, aunque ya no haya shared_ptr que sea el propietario del recurso.
void foo() { std::string bar; // // more code here // }
No importa lo que suceda, la barra se eliminará correctamente una vez que el alcance de la función foo () haya quedado atrás.
Internamente, las implementaciones std :: string a menudo usan punteros contados de referencia. Entonces, la cadena interna solo necesita copiarse cuando una de las copias de las cadenas cambia. Por lo tanto, un puntero inteligente contado de referencia hace posible 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.