c++ - Comprender el significado del término y el concepto: RAII(Adquisición de recursos es inicialización)
garbage-collection resource-management (11)
¿Podrían los desarrolladores de C ++ darnos una buena descripción de lo que es RAII, por qué es importante y si puede tener alguna relevancia para otros idiomas?
Lo sé un poco. Creo que significa "Adquisición de recursos es inicialización". Sin embargo, ese nombre no coincide con mi comprensión (posiblemente incorrecta) de lo que RAII es: me da la impresión de que RAII es una forma de inicializar objetos en la pila de manera que, cuando esas variables se salgan de alcance, los destructores automáticamente ser llamado causando que los recursos sean limpiados.
Entonces, ¿por qué no se llama "usar la pila para activar la limpieza" (UTSTTC :)? ¿Cómo se llega desde allí a "RAII"?
¿Y cómo puedes hacer algo en la pila que cause la limpieza de algo que vive en el montón? Además, ¿hay casos en los que no puedas usar RAII? ¿Alguna vez te has sentido deseoso de recolectar basura? ¿Al menos un recolector de basura que podrías usar para algunos objetos mientras permites que otros sean administrados?
Gracias.
¿Y cómo puedes hacer algo en la pila que cause la limpieza de algo que vive en el montón?
class int_buffer
{
size_t m_size;
int * m_buf;
public:
int_buffer( size_t size )
: m_size( size ), m_buf( 0 )
{
if( m_size > 0 )
m_buf = new int[m_size]; // will throw on failure by default
}
~int_buffer()
{
delete[] m_buf;
}
/* ...rest of class implementation...*/
};
void foo()
{
int_buffer ib(20); // creates a buffer of 20 bytes
std::cout << ib.size() << std::endl;
} // here the destructor is called automatically even if an exception is thrown and the memory ib held is freed.
Cuando aparece una instancia de int_buffer, debe tener un tamaño y asignará la memoria necesaria. Cuando sale del alcance, se llama destructor. Esto es muy útil para cosas como objetos de sincronización. Considerar
class mutex
{
// ...
take();
release();
class mutex::sentry
{
mutex & mm;
public:
sentry( mutex & m ) : mm(m)
{
mm.take();
}
~sentry()
{
mm.release();
}
}; // mutex::sentry;
};
mutex m;
int getSomeValue()
{
mutex::sentry ms( m ); // blocks here until the mutex is taken
return 0;
} // the mutex is released in the destructor call here.
Además, ¿hay casos en los que no puedas usar RAII?
No en realidad no.
¿Alguna vez te has sentido deseoso de recolectar basura? ¿Al menos un recolector de basura que podrías usar para algunos objetos mientras permites que otros sean administrados?
Nunca. La recolección de basura solo resuelve un subconjunto muy pequeño de la administración dinámica de recursos.
Entonces, ¿por qué no se llama "usar la pila para activar la limpieza" (UTSTTC :)?
RAII le dice qué hacer: ¡Adquiera su recurso en un constructor! Yo agregaría: un recurso, un constructor. UTSTTC es solo una aplicación de eso, RAII es mucho más.
La administración de recursos apesta. Aquí, el recurso es todo lo que necesita limpieza después de su uso. Los estudios de proyectos en muchas plataformas muestran que la mayoría de los errores están relacionados con la administración de recursos, y es particularmente malo en Windows (debido a los muchos tipos de objetos y asignadores).
En C ++, la administración de recursos es particularmente complicada debido a la combinación de excepciones y plantillas (estilo C ++). Para echar un vistazo debajo del capó, vea GOTW8 ).
C ++ garantiza que se llame al destructor si y solo si el constructor tuvo éxito. Confiando en eso, RAII puede resolver muchos problemas desagradables que el programador promedio podría no conocer. Aquí hay algunos ejemplos más allá de "mis variables locales serán destruidas cada vez que regrese".
Comencemos con una clase FileHandle
demasiado simplista empleando RAII:
class FileHandle
{
FILE* file;
public:
explicit FileHandle(const char* name)
{
file = fopen(name);
if (!file)
{
throw "MAYDAY! MAYDAY";
}
}
~FileHandle()
{
// The only reason we are checking the file pointer for validity
// is because it might have been moved (see below).
// It is NOT needed to check against a failed constructor,
// because the destructor is NEVER executed when the constructor fails!
if (file)
{
fclose(file);
}
}
// The following technicalities can be skipped on the first read.
// They are not crucial to understanding the basic idea of RAII.
// However, if you plan to implement your own RAII classes,
// it is absolutely essential that you read on :)
// It does not make sense to copy a file handle,
// hence we disallow the otherwise implicitly generated copy operations.
FileHandle(const FileHandle&) = delete;
FileHandle& operator=(const FileHandle&) = delete;
// The following operations enable transfer of ownership
// and require compiler support for rvalue references, a C++0x feature.
// Essentially, a resource is "moved" from one object to another.
FileHandle(FileHandle&& that)
{
file = that.file;
that.file = 0;
}
FileHandle& operator=(FileHandle&& that)
{
file = that.file;
that.file = 0;
return *this;
}
}
Si la construcción falla (con una excepción), no se llama a ninguna otra función miembro, ni siquiera al destructor.
RAII evita el uso de objetos en un estado no válido. ya hace la vida más fácil incluso antes de que usemos el objeto.
Ahora, echemos un vistazo a los objetos temporales:
void CopyFileData(FileHandle source, FileHandle dest);
void Foo()
{
CopyFileData(FileHandle("C://source"), FileHandle("C://dest"));
}
Se manejan tres casos de error: no se puede abrir ningún archivo, solo se puede abrir un archivo, se pueden abrir ambos archivos pero se produjo un error al copiar los archivos. En una implementación sin RAII, Foo
tendría que manejar los tres casos explícitamente.
RAII libera recursos que fueron adquiridos, incluso cuando se adquieren múltiples recursos dentro de una declaración.
Ahora, agreguemos algunos objetos:
class Logger
{
FileHandle original, duplex; // this logger can write to two files at once!
public:
Logger(const char* filename1, const char* filename2)
: original(filename1), duplex(filename2)
{
if (!filewrite_duplex(original, duplex, "New Session"))
throw "Ugh damn!";
}
}
El constructor de Logger
fallará si falla el constructor original
(porque filename1
no se pudo abrir), el constructor de duplex
falla (porque el filename2
no se pudo abrir) o si se Logger
un error al escribir en los archivos dentro del cuerpo constructor de Logger
. En cualquiera de estos casos, no se llamará al destructor de Logger
, por lo que no podemos confiar en que el destructor de Logger
libere los archivos. Pero si se construyó el original
, se llamará a su destructor durante la limpieza del constructor de Logger
.
RAII simplifica la limpieza después de la construcción parcial.
Puntos negativos:
¿Puntos negativos? Todos los problemas se pueden resolver con RAII y punteros inteligentes ;-)
RAII es a veces difícil de manejar cuando se necesita una adquisición diferida, empujando los objetos agregados al montón.
Imagine que el Logger necesita un SetTargetFile(const char* target)
. En ese caso, el identificador, que aún debe ser miembro de Logger
, debe residir en el montón (por ejemplo, en un puntero inteligente, para desencadenar la destrucción del asa adecuadamente).
Nunca he deseado recolección de basura realmente. Cuando hago C #, a veces siento un momento de felicidad que simplemente no me tiene que importar, pero echo mucho más de menos todos los juguetes geniales que se pueden crear a través de la destrucción determinista. ( IDisposable
simplemente no lo corta).
He tenido una estructura particularmente compleja que podría haberse beneficiado con GC, donde los punteros inteligentes "simples" causarían referencias circulares en varias clases. Nos entrometimos equilibrando cuidadosamente los indicadores fuertes y débiles, pero cada vez que queremos cambiar algo, tenemos que estudiar una gran tabla de relaciones. GC podría haber sido mejor, pero algunos de los componentes contenían recursos que deberían publicarse lo antes posible.
Una nota en el ejemplo de FileHandle: no fue pensada para ser completa, solo una muestra, pero resultó incorrecta. Gracias Johannes Schaub por señalar y FredOverflow por convertirlo en una solución correcta de C ++ 0x. Con el tiempo, me he conformado con el enfoque documentado aquí .
El problema con la recolección de basura es que pierdes la destrucción determinista que es crucial para RAII. Una vez que una variable se sale del alcance, le corresponde al recolector de basura cuando se reclamará el objeto. El recurso retenido por el objeto continuará retenido hasta que se llame al destructor.
Estoy de acuerdo con cpitis. Pero me gustaría agregar que los recursos pueden ser cualquier cosa, no solo memoria. El recurso podría ser un archivo, una sección crítica, un hilo o una conexión de base de datos.
Se llama Adquisición de recursos Es inicialización porque el recurso se adquiere cuando se construye el objeto que controla el recurso. Si el constructor falló (es decir, debido a una excepción) el recurso no se adquiere. Luego, una vez que el objeto sale del alcance, se libera el recurso. c ++ garantiza que todos los objetos en la pila que se hayan construido con éxito serán destruidos (esto incluye constructores de clases base y miembros incluso si el constructor de la superclase falla).
Lo racional detrás de RAII es hacer que la excepción de adquisición de recursos sea segura. Que todos los recursos adquiridos se publiquen correctamente, sin importar dónde se produzca una excepción. Sin embargo, esto depende de la calidad de la clase que adquiere el recurso (debe ser una excepción segura y esto es difícil).
Hay excelentes respuestas, así que solo agrego algunas cosas olvidadas.
0. RAII se trata de alcances
RAII se trata de ambos:
- adquiriendo un recurso (no importa qué recurso) en el constructor, y des-adquiriéndolo en el destructor.
- haciendo que el constructor se ejecute cuando se declara la variable, y el destructor se ejecuta automáticamente cuando la variable sale del alcance.
Otros ya respondieron sobre eso, así que no daré más detalles.
1. Al codificar en Java o C #, ya usas RAII ...
MONSIEUR JOURDAIN: ¡Qué! Cuando digo: "Nicole, tráeme mis pantuflas y dame mi gorro de dormir", ¿eso es prosa?
MAESTRO DE FILOSOFÍA: Sí, señor.
MONSIEUR JOURDAIN: Durante más de cuarenta años he estado hablando en prosa sin saber nada al respecto, y estoy muy agradecido por haberme enseñado eso.
- Molière: The Middle Class Gentleman, Act 2, Scene 4
Como Monsieur Jourdain hizo con la prosa, C # e incluso las personas de Java ya usan RAII, pero de forma oculta. Por ejemplo, el siguiente código Java (que se escribe de la misma manera en C # reemplazando synchronized
con lock
):
void foo()
{
// etc.
synchronized(someObject)
{
// if something throws here, the lock on someObject will
// be unlocked
}
// etc.
}
... ya está utilizando RAII: la adquisición mutex se realiza con la palabra clave ( synchronized
o lock
), y la anulación de la adquisición se realizará al salir del alcance.
Es tan natural en su notación que casi no requiere explicación, incluso para las personas que nunca escucharon acerca de RAII.
La ventaja que tiene C ++ sobre Java y C # es que cualquier cosa se puede hacer usando RAII. Por ejemplo, no hay un equivalente de compilación directa de synchronized
ni lock
en C ++, pero aún podemos tenerlos.
En C ++, se escribiría:
void foo()
{
// etc.
{
Lock lock(someObject) ; // lock is an object of type Lock whose
// constructor acquires a mutex on
// someObject and whose destructor will
// un-acquire it
// if something throws here, the lock on someObject will
// be unlocked
}
// etc.
}
que se puede escribir fácilmente en el modo Java / C # (usando macros C ++):
void foo()
{
// etc.
LOCK(someObject)
{
// if something throws here, the lock on someObject will
// be unlocked
}
// etc.
}
2. RAII tiene usos alternativos
CONEJO BLANCO: [cantando] llego tarde / llego tarde / para una cita muy importante. / No hay tiempo para decir "Hola". / Adiós. / Llego tarde, llego tarde, llego tarde.
- Alicia en el país de las maravillas (versión de Disney, 1951)
Usted sabe cuándo se llamará al constructor (en la declaración del objeto), y usted sabrá cuándo se llamará al destructor correspondiente (a la salida del alcance), para que pueda escribir un código casi mágico con una línea. Bienvenido a C ++ wonderland (al menos, desde el punto de vista de un desarrollador de C ++).
Por ejemplo, puedes escribir un objeto contador (lo dejo como ejercicio) y usarlo simplemente declarando su variable, como se utilizó el objeto de bloqueo anterior:
void foo()
{
double timeElapsed = 0 ;
{
Counter counter(timeElapsed) ;
// do something lengthy
}
// now, the timeElapsed variable contain the time elapsed
// from the Counter''s declaration till the scope exit
}
que por supuesto, se puede escribir, de nuevo, la forma Java / C # usando una macro:
void foo()
{
double timeElapsed = 0 ;
COUNTER(timeElapsed)
{
// do something lengthy
}
// now, the timeElapsed variable contain the time elapsed
// from the Counter''s declaration till the scope exit
}
3. ¿Por qué carece C ++ finally
?
[GRITANDO] ¡Es la cuenta final !
- Europa: la cuenta regresiva final (lo siento, me quedé sin comillas, aquí ... :-)
La cláusula finally
se usa en C # / Java para gestionar la eliminación de recursos en caso de que se cierre el alcance (ya sea mediante una return
o una excepción lanzada).
Los lectores astutos de especificaciones habrán notado que C ++ no tiene cláusula finally. Y esto no es un error, porque C ++ no lo necesita, ya que RAII ya maneja la eliminación de recursos. (Y créanme, escribir un destructor de C ++ es mucho más fácil que escribir la cláusula Java finally correcta, o incluso el método Dispose correcto de C #).
Aún así, a veces, una cláusula final sería genial. ¿Podemos hacerlo en C ++? ¡Si podemos! Y de nuevo con un uso alternativo de RAII.
Conclusión: RAII es más que filosofía en C ++: es C ++
RAII? ESTO ES C ++ !!!
- Comentario indignado del desarrollador de C ++, copiado descaradamente por un oscuro rey de Esparta y sus 300 amigos
Cuando alcanzas cierto nivel de experiencia en C ++, comienzas a pensar en términos de RAII , en términos de construtors y destrucción automática de destructores .
Empiezas a pensar en términos de ámbitos y los {
y }
caracteres se convierten en los más importantes de tu código.
Y casi todo encaja bien en términos de RAII: seguridad de excepciones, mutexes, conexiones a bases de datos, solicitudes de bases de datos, conexión al servidor, relojes, controladores de sistema operativo, etc., y por último, pero no menos importante, la memoria.
La parte de la base de datos no es despreciable, ya que si acepta pagar el precio, puede incluso escribir en un estilo de " programación transaccional ", ejecutando líneas y líneas de código hasta decidir, al final, si desea comprometer todos los cambios , o, si no es posible, tener todos los cambios revertidos (siempre que cada línea satisfaga al menos la Garantía de Fuerte Excepción). (ver la segunda parte de este artículo de Herb''s Sutter para la programación transaccional).
Y como un rompecabezas, todo encaja.
RAII es tanto parte de C ++, C ++ no podría ser C ++ sin él.
Esto explica por qué los desarrolladores experimentados de C ++ están tan entusiasmados con RAII, y por qué RAII es lo primero que buscan al probar otro idioma.
Y explica por qué el Garbage Collector, si bien es una pieza de tecnología magnífica en sí misma, no es tan impresionante desde el punto de vista de un desarrollador de C ++:
- RAII ya maneja la mayoría de los casos manejados por un GC
- Un GC ofrece mejores resultados que RAII con referencias circulares sobre objetos gestionados puros (mitigados por usos inteligentes de indicadores débiles)
- Todavía un GC está limitado a la memoria, mientras que RAII puede manejar cualquier tipo de recurso.
- Como se describió anteriormente, RAII puede hacer mucho, mucho más ...
Me gustaría ponerlo un poco más fuerte que las respuestas anteriores.
RAII, Adquisición de recursos Es inicialización significa que todos los recursos adquiridos deben adquirirse en el contexto de la inicialización de un objeto. Esto prohíbe la adquisición de recursos "desnudos". El fundamento es que la limpieza en C ++ funciona sobre la base de objeto, no de base de llamada de función. Por lo tanto, toda la limpieza debe hacerse por objetos, no por llamadas a funciones. En este sentido, C ++ está más orientado a objetos, por ejemplo, Java. La limpieza de Java se basa en llamadas a funciones en cláusulas finally
.
RAII es un acrónimo de Resource Acquisition Is Initialization.
Esta técnica es muy exclusiva de C ++ debido a su compatibilidad con Constructores y Destructores y casi automáticamente los constructores que coinciden con los argumentos que se pasan o el peor caso se llama el constructor predeterminado y destructores si explícitamente se llama. que se agrega mediante el compilador C ++ se invoca si no se escribió un destructor explícitamente para una clase C ++. Esto ocurre solo para los objetos C ++ que se administran automáticamente, lo que significa que no están utilizando la tienda gratuita (memoria asignada / desasignada usando nuevos, nuevos [] / eliminar, eliminar [] operadores C ++).
La técnica RAII hace uso de esta característica de objeto administrado automáticamente para manejar los objetos que se crean en el heap / free-store pidiendo explícitamente más memoria usando new / new [], que se debe destruir explícitamente llamando a delete / delete [] . La clase del objeto autogestionado envolverá este otro objeto que se crea en la memoria heap / free-store. Por lo tanto, cuando se ejecuta el constructor del objeto autogestionado, el objeto envuelto se crea en la memoria heap / free-store y cuando el manejador del objeto administrado automáticamente sale del alcance, el destructor de ese objeto autoadministrado se llama automáticamente en el que el envuelto el objeto se destruye usando delete. Con los conceptos de OOP, si envuelve dichos objetos dentro de otra clase en ámbito privado, no tendría acceso a los miembros y métodos de clases envueltos y esta es la razón por la cual los punteros inteligentes (también conocidos como clases de manejo) están diseñados para. Estos punteros inteligentes exponen el objeto envuelto como un objeto tipeado al mundo externo y allí al permitir invocar cualquier miembro / método del que esté compuesto el objeto de memoria expuesta. Tenga en cuenta que los indicadores inteligentes tienen varios sabores basados en diferentes necesidades. Debería consultar la programación Modern C ++ de Andrei Alexandrescu o la implementación / documentación de la biblioteca boost (www.boostorg) shared_ptr.hpp para obtener más información al respecto. Espero que esto te ayude a entender RAII.
RAII está utilizando la semántica de destructores de C ++ para administrar los recursos. Por ejemplo, considere un puntero inteligente. Tiene un constructor parametrizado del puntero que inicializa este puntero con la dirección del objeto. Usted asigna un puntero en la pila:
SmartPointer pointer( new ObjectClass() );
Cuando el puntero inteligente se sale del alcance, el destructor de la clase de puntero elimina el objeto conectado. El puntero se asigna en pila y el objeto se asigna en el montón.
Hay ciertos casos cuando RAII no ayuda. Por ejemplo, si utiliza punteros inteligentes de recuento de referencias (como boost :: shared_ptr) y crea una estructura similar a un gráfico con un ciclo, corre el riesgo de tener una fuga de memoria porque los objetos de un ciclo se evitarán mutuamente. La recolección de basura ayudaría en contra de esto.
RAII proviene de Asignación de recursos Es inicialización. Básicamente, significa que cuando un constructor termina la ejecución, el objeto construido está completamente inicializado y listo para usar. También implica que el destructor liberará todos los recursos (por ejemplo, memoria, recursos del sistema operativo) propiedad del objeto.
En comparación con los lenguajes / tecnologías recogidos de basura (por ejemplo, Java, .NET), C ++ permite un control total de la vida de un objeto. Para un objeto asignado a la pila, sabrá cuándo se llamará al destructor del objeto (cuando la ejecución se salga del alcance), cosa que realmente no se controla en caso de recolección de basura. Incluso utilizando punteros inteligentes en C ++ (por ejemplo, boost :: shared_ptr), sabrá que cuando no se hace referencia al objeto puntiagudo, se llamará al destructor de ese objeto.
Ya hay muchas buenas respuestas aquí, pero me gustaría agregar:
Una explicación simple de RAII es que, en C ++, un objeto asignado en la pila se destruye cada vez que sale del alcance. Eso significa que se llamará a un destructor de objetos y puede hacer toda la limpieza necesaria.
Eso significa que, si un objeto se crea sin "nuevo", no se requiere "eliminar". Y esta es también la idea detrás de "punteros inteligentes": residen en la pila y esencialmente envuelven un objeto basado en un montón.