¿Qué se entiende por adquisición de recursos es inicialización (RAII)?

Respuestas:

374

Es un nombre realmente terrible para un concepto increíblemente poderoso, y quizás una de las cosas número 1 que los desarrolladores de C ++ pierden cuando cambian a otros idiomas. Ha habido un poco de movimiento para tratar de cambiar el nombre de este concepto como Gestión de recursos limitada por alcance , aunque todavía no parece haberse dado cuenta.

Cuando decimos 'Recurso' no solo nos referimos a la memoria: podrían ser identificadores de archivos, zócalos de red, identificadores de bases de datos, objetos GDI ... En resumen, cosas de las que tenemos un suministro finito y, por lo tanto, debemos ser capaces de controlar su uso. El aspecto "vinculado al alcance" significa que la vida útil del objeto está vinculada al alcance de una variable, por lo que cuando la variable se sale del alcance, el destructor liberará el recurso. Una propiedad muy útil de esto es que ofrece una mayor seguridad de excepción. Por ejemplo, compara esto:

RawResourceHandle* handle=createNewResource();
handle->performInvalidOperation();  // Oops, throws exception
...
deleteResource(handle); // oh dear, never gets called so the resource leaks

Con el RAII

class ManagedResourceHandle {
public:
   ManagedResourceHandle(RawResourceHandle* rawHandle_) : rawHandle(rawHandle_) {};
   ~ManagedResourceHandle() {delete rawHandle; }
   ... // omitted operator*, etc
private:
   RawResourceHandle* rawHandle;
};

ManagedResourceHandle handle(createNewResource());
handle->performInvalidOperation();

En este último caso, cuando se lanza la excepción y la pila se desenrolla, las variables locales se destruyen, lo que garantiza que nuestro recurso se limpie y no se filtre.

the_mandrill
fuente
2
@the_mandrill: Probé ideone.com/1Jjzuc este programa. Pero no hay llamada de destructor. Tomdalling.com/blog/software-design/… dice que C ++ garantiza que se llamará al destructor de objetos en la pila, incluso si se produce una excepción. Entonces, ¿por qué el destructor no se ejecutó aquí? ¿Mi recurso se filtró o nunca se liberará o liberará?
Destructor
8
Se lanza una excepción, pero no la está atrapando, por lo que la aplicación finaliza. Si termina con un try {} catch () {} entonces funciona como se esperaba: ideone.com/xm2GR9
the_mandrill
2
No estoy seguro de si Scope-Boundes la mejor opción de nombre aquí, ya que los especificadores de clase de almacenamiento junto con el alcance determinan la duración de almacenamiento de una entidad. Reducirlo a un límite de alcance puede ser una simplificación útil, sin embargo, no es 100% preciso
SebNag
125

Este es un lenguaje de programación que significa brevemente que usted

  • encapsular un recurso en una clase (cuyo constructor generalmente, pero no necesariamente **, adquiere el recurso, y su destructor siempre lo libera)
  • usar el recurso a través de una instancia local de la clase *
  • el recurso se libera automáticamente cuando el objeto se sale del alcance

Esto garantiza que pase lo que pase mientras el recurso está en uso, eventualmente se liberará (ya sea debido a un retorno normal, la destrucción del objeto que lo contiene o una excepción lanzada).

Es una buena práctica ampliamente utilizada en C ++, porque además de ser una forma segura de manejar los recursos, también hace que su código sea mucho más limpio, ya que no necesita mezclar el código de manejo de errores con la funcionalidad principal.

* Actualización: "local" puede significar una variable local o una variable miembro no estática de una clase. En el último caso, la variable miembro se inicializa y se destruye con su objeto propietario.

** Actualización2: como señaló @sbi, el recurso, aunque a menudo se asigna dentro del constructor, también se puede asignar fuera y pasar como un parámetro.

Péter Török
fuente
1
AFAIK, el acrónimo no implica que el objeto tenga que estar en una variable local (pila). Podría ser una variable miembro de otro objeto, de modo que cuando se destruye el objeto 'retenido', también se destruye el objeto miembro y se libera el recurso. De hecho, creo que el acrónimo significa específicamente solo que no hay open()/ close()métodos para inicializar y liberar el recurso, solo el constructor y el destructor, por lo que la 'retención' del recurso es solo la vida útil del objeto, sin importar si esa vida es manejado por el contexto (pila) o explícitamente (asignación dinámica)
Javier
1
En realidad, nada dice que el recurso debe ser adquirido en el constructor. Las secuencias de archivos, las cadenas y otros contenedores lo hacen, pero el recurso podría pasar al constructor, como suele ser el caso con los punteros inteligentes. Dado que la suya es la respuesta más votada, es posible que desee solucionar esto.
sbi
No es un acrónimo, es una abreviatura. La mayoría de la gente del IIRC lo pronuncia "ar ey ay ay", por lo que realmente no califica para un acrónimo como decir DARPA, que se pronuncia DARPA en lugar de deletrear. Además, diría que RAII es un paradigma más que un mero idioma.
dtech
@ Peter Torok: Intenté ideone.com/1Jjzuc este programa. Pero no hay llamada de destructor. El tomdalling.com/blog/software-design/... dice que las garantías C ++ que el destructor de objetos en la pila serán llamados, incluso si se produce una excepción. Entonces, ¿por qué el destructor no se ejecutó aquí? ¿Mi recurso se filtró o nunca se liberará o liberará?
Destructor
50

"RAII" significa "Adquisición de recursos es inicialización" y en realidad es un nombre poco apropiado, ya que no se trata de la adquisición de recursos (y la inicialización de un objeto), sino de liberar el recurso (mediante la destrucción de un objeto )
Pero RAII es el nombre que tenemos y se queda.

En esencia, el modismo presenta recursos de encapsulación (fragmentos de memoria, archivos abiertos, mutexes desbloqueados, lo que sea) en objetos locales y automáticos , y tiene el destructor de ese objeto que libera el recurso cuando el objeto se destruye en el fin del alcance al que pertenece:

{
  raii obj(acquire_resource());
  // ...
} // obj's dtor will call release_resource()

Por supuesto, los objetos no siempre son objetos locales, automáticos. También podrían ser miembros de una clase:

class something {
private:
  raii obj_;  // will live and die with instances of the class
  // ... 
};

Si tales objetos manejan la memoria, a menudo se los llama "punteros inteligentes".

Hay muchas variaciones de esto. Por ejemplo, en los primeros fragmentos de código surge la pregunta de qué sucedería si alguien quisiera copiar obj. La salida más fácil sería simplemente no permitir la copia. std::unique_ptr<>, un puntero inteligente para ser parte de la biblioteca estándar como se presenta en el próximo estándar C ++, hace esto.
Otro puntero inteligente de este tipo, std::shared_ptrpresenta la "propiedad compartida" del recurso (un objeto asignado dinámicamente) que posee. Es decir, se puede copiar libremente y todas las copias se refieren al mismo objeto. El puntero inteligente realiza un seguimiento de cuántas copias se refieren al mismo objeto y lo eliminará cuando se destruya la última.
Una tercera variante es presentada porstd::auto_ptr que implementa una especie de semántica de movimiento: un objeto es propiedad de un solo puntero, e intentar copiar un objeto dará como resultado (mediante piratería de sintaxis) la transferencia de la propiedad del objeto al objetivo de la operación de copia.

sbi
fuente
44
std::auto_ptres la versión obsoleta de std::unique_ptr. std::auto_ptrtipo de semántica de movimiento simulada tanto como fue posible en C ++ 98, std::unique_ptrutiliza la nueva semántica de movimiento de C ++ 11. Se creó una nueva clase porque la semántica de movimiento de C ++ 11 es más explícita (requiere std::moveexcepto de temporal) mientras que se omitió cualquier copia de no const in std::auto_ptr.
Jan Hudec
@JiahaoCai: Una vez, hace muchos años (en Usenet), el propio Stroustrup lo dijo.
sbi
21

La vida útil de un objeto está determinada por su alcance. Sin embargo, a veces necesitamos, o es útil, crear un objeto que viva independientemente del alcance donde fue creado. En C ++, el operador newse utiliza para crear dicho objeto. Y para destruir el objeto, deletese puede usar el operador . Los objetos creados por el operador newse asignan dinámicamente, es decir, se asignan en memoria dinámica (también llamada almacenamiento dinámico o almacenamiento gratuito ). Por lo tanto, un objeto creado por newcontinuará existiendo hasta que se destruya explícitamente usando delete.

Algunos errores que pueden ocurrir al usar newy deleteson:

  • Objeto filtrado (o memoria): se usa newpara asignar un objeto y olvidarlo delete.
  • Eliminación prematura (o referencia colgante ): mantener otro puntero sobre un objeto, deleteel objeto, y luego usar el otro puntero.
  • Doble eliminación : intentando deleteun objeto dos veces.

En general, se prefieren las variables de ámbito. Sin embargo, RAII puede usarse como una alternativa newy deletehacer que un objeto viva independientemente de su alcance. Dicha técnica consiste en llevar el puntero al objeto que se asignó en el montón y colocarlo en un objeto controlador / administrador . Este último tiene un destructor que se encargará de destruir el objeto. Esto garantizará que el objeto esté disponible para cualquier función que desee acceder a él, y que el objeto se destruya cuando la vida útil del objeto de manejo finalice , sin la necesidad de una limpieza explícita.

Ejemplos de la biblioteca estándar de C ++ que usan RAII son std::stringy std::vector.

Considere esta pieza de código:

void fn(const std::string& str)
{
    std::vector<char> vec;
    for (auto c : str)
        vec.push_back(c);
    // do something
}

cuando creas un vector y empujas elementos a él, no te importa asignar y desasignar dichos elementos. El vector utiliza newpara asignar espacio para sus elementos en el montón y deletepara liberar ese espacio. Como usuario del vector, no le importan los detalles de implementación y confiará en que el vector no se filtre. En este caso, el vector es el objeto manipulador de de sus elementos.

Otros ejemplos de la biblioteca estándar que RAII uso son std::shared_ptr, std::unique_ptry std::lock_guard.

Otro nombre para esta técnica es SBRM , abreviatura de Scope-Bound Resource Management .

elmiomar
fuente
1
"SBRM" tiene mucho más sentido para mí. Llegué a esta pregunta porque pensé que entendía RAII pero el nombre me estaba desanimando, al escuchar que se describía en su lugar como "Gestión de recursos vinculada al alcance" me hizo darme cuenta de inmediato de que realmente entendía el concepto.
JShorthouse
No estoy seguro de por qué esto no fue marcado como la respuesta a la pregunta. Es una respuesta muy completa y bien escrita, gracias @elmiomar
Abdelrahman Shoman
13

El libro C ++ Programming with Design Patterns Revealed describe RAII como:

  1. Adquiriendo todos los recursos
  2. Usando recursos
  3. Liberando recursos

Dónde

  • Los recursos se implementan como clases, y todos los punteros tienen envoltorios de clase a su alrededor (haciéndolos punteros inteligentes).

  • Los recursos se adquieren invocando a sus constructores y se liberan implícitamente (en orden inverso a la adquisición) invocando a sus destructores.

Dennis
fuente
1
@Brandin He editado mi publicación para que los lectores se centren en el contenido que importa, en lugar de debatir el área gris de la ley de derechos de autor de lo que constituye un uso justo.
Dennis
7

Hay tres partes en una clase RAII:

  1. El recurso se abandona en el destructor
  2. Las instancias de la clase se asignan en pila
  3. El recurso se adquiere en el constructor. Esta parte es opcional, pero común.

RAII significa "Adquisición de recursos es inicialización". La parte de "adquisición de recursos" de RAII es donde comienza algo que debe terminar más tarde, como:

  1. Abrir un archivo
  2. Asignando un poco de memoria
  3. Adquirir un candado

La parte "es inicialización" significa que la adquisición ocurre dentro del constructor de una clase.

https://www.tomdalling.com/blog/software-design/resource-acquisition-is-initialisation-raii-explained/

Mohammad Moridi
fuente
5

La gestión manual de la memoria es una pesadilla que los programadores han estado inventando formas de evitar desde la invención del compilador. Los lenguajes de programación con recolectores de basura facilitan la vida, pero a costa del rendimiento. En este artículo: Eliminando el recolector de basura: a la manera RAII , el ingeniero de Toptal Peter Goodspeed-Niklaus nos da un vistazo a la historia de los recolectores de basura y explica cómo las nociones de propiedad y préstamo pueden ayudar a eliminar los recolectores de basura sin comprometer sus garantías de seguridad.

Dmitry Pavlov
fuente