En C ++, el programador debe decidir cómo se liberarán los recursos utilizados; no hay herramientas automáticas como el recolector de basura. El artículo analiza las posibles soluciones a este problema, examina en detalle los posibles problemas, así como una serie de problemas relacionados.
Tabla de contenidos
Introduccion
La gestión de recursos es algo que un programador de C ++ tiene que hacer todo el tiempo. Los recursos incluyen bloques de memoria, objetos del núcleo del sistema operativo, bloqueos de subprocesos múltiples, conexiones de red, conexiones de bases de datos y cualquier objeto creado en la memoria dinámica. El acceso al recurso es a través de un descriptor, el tipo de descriptor suele ser un puntero o uno de sus alias ( HANDLE
, etc.), a veces todo (descriptores de archivo UNIX). Después de usar el recurso, debe liberarlo; de lo contrario, tarde o temprano, una aplicación que no libera recursos (y posiblemente otras aplicaciones) se quedará sin recursos. Este problema es muy grave, podemos decir que una de las características clave de .NET, Java y varias otras plataformas es un sistema unificado de gestión de recursos basado en la recolección de basura.
Las características orientadas a objetos de C ++ conducen naturalmente a la siguiente solución: la clase que gestiona el recurso contiene el descriptor del recurso como miembro, inicializa el descriptor cuando se captura el recurso y lo libera en el destructor. Pero después de algún pensamiento (o experiencia) llega la comprensión de que no es tan simple. Y el principal problema es la semántica de la copia. Si la clase que administra el recurso utiliza el constructor de copias generado por el compilador predeterminado, luego de copiar el objeto obtendremos dos copias del identificador del mismo recurso. Si un objeto libera un recurso, entonces el segundo podrá intentar usar o liberar el recurso ya liberado, que en cualquier caso es incorrecto y puede conducir al llamado comportamiento indefinido, es decir, cualquier cosa puede suceder, por ejemplo, una terminación anormal del programa.
Afortunadamente, en C ++, un programador puede controlar completamente el proceso de copia definiendo un constructor de copia y un operador de asignación de copia por sí mismo, lo que nos permite resolver el problema anterior, y generalmente no de una manera. La implementación de la copia debe estar estrechamente relacionada con el mecanismo de liberación del recurso, y colectivamente llamaremos a esto la estrategia de propiedad de la copia. La llamada "regla de los Tres Grandes" es bien conocida, y establece que si un programador define al menos una de las tres operaciones: constructor de copia, operador de asignación de copia o destructor, debe definir las tres operaciones. Las estrategias de propiedad de copia solo especifican cómo hacer esto. Hay cuatro estrategias básicas de propiedad de copias.
1. Estrategias básicas de propiedad de copias
Antes de la captura del recurso o después de su lanzamiento, el descriptor debe tomar un valor especial que indique que no está asociado con el recurso. Por lo general, esto es cero, a veces -1, emitido a un tipo de descriptor. En cualquier caso, dicho descriptor se llamará cero. La clase que gestiona el recurso debe reconocer el descriptor nulo y no intentar usar o liberar el recurso en este caso.
1.1. Estrategia de prohibición de copia
Esta es la estrategia más simple. En este caso, simplemente está prohibido copiar y asignar instancias de clase. El destructor libera el recurso capturado. En C ++, prohibir la copia no es difícil, la clase debe declarar, pero no definir, el constructor de copia cerrada y el operador de asignación de copia.
class X { private: X(const X&); X& operator=(const X&);
El compilador y el enlazador frustran los intentos de copia.
El estándar C ++ 11 ofrece una sintaxis especial para este caso:
class X { public: X(const X&) = delete; X& operator=(const X&) = delete;
Esta sintaxis es más visual y proporciona mensajes más comprensibles al compilador cuando intenta copiar.
En la versión anterior de la biblioteca estándar (C ++ 98), las clases de flujos de entrada / salida ( std::fstream
, etc.) usaban la estrategia de prohibición de copia, y en Windows, muchas clases de MFC ( CFile
, CEvent
, CMutex
, etc.). En la biblioteca estándar de C ++ 11, algunas clases utilizan esta estrategia para admitir la sincronización multiproceso.
1.2. Estrategia de propiedad exclusiva
En este caso, al implementar la copia y la asignación, el descriptor de recursos se mueve del objeto de origen al objeto de destino, es decir, permanece en una sola copia. Después de copiar o asignar, el objeto fuente tiene un descriptor nulo y no puede usar el recurso. El destructor libera el recurso capturado. Los términos propiedad exclusiva o estricta [Josuttis] también se utilizan para esta estrategia; Andrei Alexandrescu utiliza el término copia destructiva. En C ++ 11, esto se hace de la siguiente manera: la copia regular y la asignación de copias están prohibidas como se describe anteriormente, y se implementa la semántica de movimiento, es decir, se definen el constructor de movimiento y el operador de asignación de movimiento. (Más sobre semántica del movimiento más adelante).
class X { public: X(const X&) = delete; X& operator=(const X&) = delete; X(X&& src) noexcept; X& operator=(X&& src) noexcept;
Por lo tanto, la estrategia de propiedad exclusiva puede considerarse una extensión de la estrategia de prohibición de copia.
En la biblioteca estándar de C ++ 11, esta estrategia utiliza el puntero inteligente std::unique_ptr<>
y algunas otras clases, por ejemplo: std::thread
, std::unique_lock<>
, así como las clases que anteriormente usaban la estrategia de prohibición de copia ( std::fstream
, etc.). En Windows, las clases de MFC que anteriormente usaban la estrategia de prohibición de copia también comenzaron a usar la estrategia de propiedad exclusiva ( CFile
, CEvent
, CMutex
, etc.).
1.3. Estrategia de copia profunda
En este caso, puede copiar y asignar instancias de clase. Es necesario definir el constructor de copia y el operador de asignación de copia, de modo que el objeto de destino copie el recurso en sí mismo del objeto de origen. Después de eso, cada objeto posee su copia del recurso, puede usar, modificar y liberar el recurso de forma independiente. El destructor libera el recurso capturado. A veces, para los objetos que usan la estrategia de copia profunda, se usa el término objetos de valor.
Esta estrategia no se aplica a todos los recursos. Se puede aplicar a los recursos asociados con un búfer de memoria, como cadenas, pero no está muy claro cómo aplicarlo a los objetos del núcleo del sistema operativo, como archivos, mutexes, etc.
La estrategia de copia profunda se utiliza en todos los tipos de cadenas de objetos, std::vector<>
y otros contenedores de la biblioteca estándar.
1.4. Estrategia de copropiedad
En este caso, puede copiar y asignar instancias de clase. Debe definir el constructor de copia y el operador de asignación de copia en el que se copia el descriptor de recursos (así como otros datos), pero no el recurso en sí. Después de eso, cada objeto tiene su propia copia del descriptor, puede usar, modificar, pero no puede liberar el recurso, siempre que haya al menos un objeto más que posea una copia del descriptor. Un recurso se libera después de que el último objeto que posee una copia del identificador se salga del alcance. Cómo se puede implementar esto se describe a continuación.
Las estrategias de copropiedad a menudo son utilizadas por los punteros inteligentes, y también es natural usarlas para recursos inmutables. El puntero inteligente std::shared_ptr<>
implementa esta estrategia en la biblioteca estándar C ++ 11.
2. Estrategia de copia profunda: problemas y soluciones
Considere una plantilla para la función de intercambio de estado de objetos de tipo T
en la biblioteca estándar C ++ 98.
template<typename T> void swap(T& a, T& b) { T tmp(a); a = b; b = tmp; }
Si el tipo T
posee un recurso y utiliza una estrategia de copia profunda, entonces tenemos tres operaciones para asignar un nuevo recurso, tres operaciones de copia y tres operaciones para liberar recursos. Si bien en la mayoría de los casos esta operación puede llevarse a cabo sin asignar nuevos recursos y copiarlos, es suficiente que los objetos intercambien datos internos, incluido un descriptor de recursos. Hay muchos ejemplos similares cuando tiene que crear copias temporales de un recurso y liberarlas inmediatamente. Una implementación tan ineficaz de las operaciones cotidianas estimuló la búsqueda de soluciones para su optimización. Consideremos las principales opciones.
2.1. Copia en el registro
Copia en escritura (COW), también llamada copia diferida, puede verse como un intento de combinar una estrategia de copia profunda y una estrategia de propiedad compartida. Inicialmente, al copiar un objeto, el descriptor del recurso se copia, sin el recurso en sí mismo, y para los propietarios el recurso se vuelve compartido y de solo lectura, pero tan pronto como algún propietario necesita modificar el recurso compartido, el recurso se copia y luego este propietario trabaja con su una copia La implementación de COW resuelve el problema del intercambio de estados: la asignación adicional de recursos y la copia no ocurren. El uso de COW es bastante popular cuando se implementan cadenas; por ejemplo, CString
(MFC, ATL). Se puede encontrar una discusión sobre las posibles formas de implementar la VAC y los problemas emergentes en [Meyers1], [Sutter]. [Guntheroth] propuso una implementación COW usando std::shared_ptr<>
. Hay problemas al implementar COW en un entorno multiproceso, por lo que está prohibido usar COW para cadenas en la biblioteca estándar de C ++ 11, consulte [Josuttis], [Guntheroth].
El desarrollo de la idea COW conduce al siguiente esquema de administración de recursos: el recurso es inmutable y administrado por objetos que usan la estrategia de propiedad compartida, si es necesario, cambie el recurso, se crea un nuevo recurso modificado adecuadamente y se devuelve un nuevo objeto propietario. Este esquema se utiliza para cadenas y otros objetos inmutables en las plataformas .NET y Java. En la programación funcional, se usa para estructuras de datos más complejas.
2.2. Definir una función de intercambio de estado para una clase
Se mostró anteriormente cuán ineficiente puede ser la función de intercambio de estado, implementada de manera directa, mediante copia y asignación. Y se usa bastante, por ejemplo, muchos algoritmos de la biblioteca estándar lo usan. Para que los algoritmos utilicen no otro std::swap()
, sino otra función específicamente definida para la clase, se deben realizar dos pasos.
1. Defina en la clase una función miembro Swap()
(el nombre no es importante) que implementa el intercambio de estados.
class X { public: void Swap(X& other) noexcept;
Debe asegurarse de que esta función no arroje excepciones; en C ++ 11, dichas funciones deben declararse como noexcept
.
2. En el mismo espacio de nombres que la clase X
(generalmente en el mismo archivo de encabezado), defina la función swap()
libre (no miembro) de la siguiente manera (el nombre y la firma son fundamentales):
inline void swap(X& a, X& b) noexcept { a.Swap(b); }
Después de eso, los algoritmos de la biblioteca estándar lo usarán, no std::swap()
. Esto proporciona un mecanismo llamado búsqueda dependiente de argumentos (ADL). Para más información sobre ADL, consulte [Dewhurst1].
En la biblioteca estándar de C ++, todos los contenedores, punteros inteligentes, así como otras clases implementan la función de intercambio de estado como se describió anteriormente.
La función de miembro Swap()
generalmente se define fácilmente: es necesario aplicar secuencialmente una operación de intercambio de estado a las bases de datos y miembros, si lo admiten, y std::swap()
contrario.
La descripción anterior está algo simplificada, una más detallada se puede encontrar en [Meyers2]. Una discusión de temas relacionados con la función de intercambio de estado también se puede encontrar en [Sutter / Alexandrescu].
La función de intercambio de estado se puede atribuir a una de las operaciones básicas de la clase. Al usarlo, puede definir con gracia otras operaciones. Por ejemplo, el operador de asignación de copia se define mediante copy y Swap()
siguiente manera:
X& X::operator=(const X& src) { X tmp(src); Swap(tmp); return *this; }
Esta plantilla se llama modismo de copia e intercambio o modismo de Herb Sutter, para más detalles ver [Sutter], [Sutter / Alexandrescu], [Meyers2]. Su modificación se puede aplicar para implementar la semántica del desplazamiento, ver secciones 2.4, 2.6.1.
2.3. Eliminar copias intermedias por el compilador
Considera la clase
class X { public: X();
Y funcion
X Foo() {
Con un enfoque directo, el retorno de la función Foo()
se realiza copiando la instancia de X
Pero los compiladores pueden eliminar la operación de copia del código, el objeto se crea directamente en el punto de llamada. Esto se llama optimización del valor de retorno (RVO). RVO ha sido utilizado por los desarrolladores de compiladores durante bastante tiempo y actualmente está arreglado en el estándar C ++ 11. Aunque la decisión sobre RVO la toma el compilador, el programador puede escribir código en función de su uso. Para hacer esto, es deseable que la función tenga un punto de retorno y el tipo de la expresión devuelta coincida con el tipo del valor de retorno de la función. En algunos casos, es aconsejable definir un constructor cerrado especial llamado "constructor computacional", para más detalles ver [Dewhurst2]. RVO también se discute en [Meyers3] y [Guntheroth].
Los compiladores pueden eliminar copias intermedias en otras situaciones.
2.4. Implementación de la semántica del desplazamiento.
La implementación de la semántica de movimiento consiste en definir un constructor de movimiento que tenga un parámetro de tipo rvalue-reference a la fuente y un operador de asignación de movimiento con el mismo parámetro.
En la Biblioteca estándar de C ++ 11, la plantilla de función de intercambio de estado se define de la siguiente manera:
template<typename T> void swap(T& a, T& b) { T tmp(std::move(a)); a = std::move(b); b = std::move(tmp); }
De acuerdo con las reglas para resolver sobrecargas de funciones que tienen parámetros del tipo rvalue-reference (ver Apéndice A), en el caso de que el tipo T
tenga un constructor móvil y un operador de asignación móvil, se utilizarán, y no habrá asignación de recursos temporales y copia. De lo contrario, se utilizará el constructor de copia y el operador de asignación de copia.
El uso de la semántica de la reubicación evita la creación de copias temporales en un contexto mucho más amplio que la función de intercambio de estado descrita anteriormente. La semántica de movimiento se aplica a cualquier valor de valor, es decir, un valor temporal sin nombre, así como al valor de retorno de una función si se creó localmente (incluido lvalue), y no se aplicó RVO. En todos estos casos, se garantiza que el objeto de origen no se puede usar de ninguna manera después del movimiento. La semántica de movimiento también se aplica al valor lvalue al que se aplica la transformación std::move()
. Pero en este caso, el programador es responsable de cómo se usarán los objetos fuente después del movimiento (ejemplo std::swap()
).
La biblioteca estándar de C ++ 11 se ha rediseñado teniendo en cuenta la semántica del movimiento. Muchas clases han agregado un constructor de movimientos y un operador de asignación de movimientos, así como otras funciones miembro, con parámetros de referencia de tipo rvalue. Por ejemplo, std::vector<T>
tiene una versión sobrecargada de void push_back(T&& src)
. Todo esto permite en muchos casos evitar crear copias temporales.
La implementación de la semántica de movimiento no cancela las definiciones de la función de intercambio de estado para una clase. Una función de intercambio de estado especialmente definida puede ser más eficiente que el estándar std::swap()
. Además, el constructor de movimientos y el operador de asignación de movimientos se definen muy fácilmente usando la función miembro del intercambio de estados de la siguiente manera (variación del idioma de copia e intercambio):
class X { public: X() noexcept {} void Swap(X& other) noexcept {} X(X&& src) noexcept : X() { Swap(src); } X& operator=(X&& src) noexcept { X tmp(std::move(src));
El constructor de movimientos y el operador de asignación de movimientos son aquellas funciones miembro para las cuales es altamente deseable asegurarse de que no arrojen excepciones y, en consecuencia, se declaren como noexcept
. Esto le permite optimizar algunas operaciones de los contenedores de la biblioteca estándar sin violar la estricta garantía de seguridad de las excepciones; consulte [Meyers3] y [Guntheroth] para obtener más detalles. La plantilla propuesta proporciona dicha garantía, siempre que el constructor predeterminado y la función miembro del intercambio de estados no arrojen excepciones.
El estándar C ++ 11 permite que el compilador genere automáticamente un constructor móvil y un operador de asignación móvil. Para ello, deben declararse utilizando la construcción "=default"
.
class X { public: X(X&&) = default; X& operator=(X&&) = default;
Las operaciones se implementan aplicando secuencialmente la operación de movimiento a las bases y los miembros de la clase, si son compatibles con el movimiento, y copie las operaciones de otra manera. Está claro que esta opción está lejos de ser siempre aceptable. Los descriptores sin formato no se mueven, pero generalmente no puede copiarlos. Bajo ciertas condiciones, el compilador puede generar independientemente un constructor móvil y un operador de asignación móvil similares, pero es mejor no aprovechar esta oportunidad, estas condiciones son bastante confusas y pueden cambiar fácilmente cuando se refina la clase. Ver [Meyers3] para más detalles.
En general, la implementación y el uso de la semántica del desplazamiento es bastante "sutil". El compilador puede aplicar la copia donde el programador espera un movimiento. Aquí hay algunas reglas para eliminar o al menos reducir la probabilidad de tal situación.
- Si es posible, use la prohibición de copia.
- Declare el constructor de movimiento y el operador de asignación de movimiento como
noexcept
. - Implementar semántica de movimiento para clases base y miembros.
- Aplique la transformación
std::move()
a los parámetros de las funciones de tipo rvalue reference.
La regla 2 se discutió anteriormente. 4 , rvalue- lvalue (. ). .
class B {
, . 6.2.1.
2.5. vs.
, RVO (. 2.3), , . ( ), , . , . C++11 - emplace()
, emplace_front()
, emplace_back()
, . , - — (variadic templates), . , C++11 — .
:
- , , .
- , , .
, .
std::vector<std::string> vs; vs.push_back(std::string(3, 'X'));
std::string
, . . , , . , [Meyers3].
2.6. Resumen
, , . - . . — : , . , , , . : , , «» .
: , , .NET Java. , Clone()
Duplicate()
.
- - , :
- .
- .
- - rvalue-.
.NET Java - , , .NET IClonable
. , .
3.
, . - , . , . Windows: , HANDLE
, COM-. DuplicateHandle()
, CloseHandle()
. COM- - IUnknown::AddRef()
IUnknown::Release()
. ATL ComPtr<>
, COM- . UNIX, C, _dup()
, .
C++11 std::shared_ptr<>
. , , , , , . , . std::shared_ptr<>
[Josuttis], [Meyers3].
: - , ( ). ( ) , . std::shared_ptr<>
std::weak_ptr<>
. . [Josuttis], [Meyers3].
- [Alexandrescu]. ( ) , [Schildt]. , .
( ) [Alger].
-. [Josuttis] [Alexandrescu].
- .NET Java. , , , .
4.
, C++ rvalue- . C++98 std::auto_ptr<>
, , , . , , ( ). C++11 rvalue- , , . C++11 std::auto_ptr><>
std::unique_ptr<>
. , [Josuttis], [Meyers3].
: - ( std::fstream
, etc.), ( std::thread
, std::unique_lock<>
, etc.). MFC , ( CFile
, CEvent
, CMutex
, etc.).
5. —
. , . , , , . , , , ( ) . , , , . ( ) , . , . — . 6.
, - -, « », - . - . , , , , - . «».
6. -
, - . , -. .
6.1.
- . , , :
- . , .
- .
- .
, , , . C++11 .
« » (resource acquisition is initialization, RAII). RAII ( ), ., [Dewhurst1]. «» RAII. , , , (immutable) RAII.
6.2.
, RAII, , , . - , , - . , , , . .
6.2.1.
, , , , :
- , .
- .
- .
- .
C++11 , , , . , - clear()
, , , . . , shrink_to_fit()
, , (. ).
, RAII, , , . , .
class X { public:
.
X x;
std::thread
.
2.4, - . , - - . .
class X {
:
X::X(X&& src) noexcept : X() { Swap(src); } X& X::operator=(X&& src) noexcept { X tmp(std::move(src));
- :
void X::Create() { X tmp();
, , , - . , , , . , .
- « », , . : , , ( ). : , . , : , , . , . [Sutter], [Sutter/Alexandrescu], [Meyers2].
, RAII .
6.2.2.
RAII . , , , , :
- , .
- .
- . , .
- .
- .
«» RAII, — . , , . 3. . «», .
6.2.3.
— . RAII , . , . , , ( -). - ( -). 6.2.1, .
6.3.
, - RAII, : . , , .
7.
, , , , . - -.
4 -:
- .
- .
- .
- .
. , - : , , - .
, . , , -, , .
- . . , (. 6.2.3). , (. 6.2.1). , . , , . , std::shared_ptr<>
.
Aplicaciones
. Rvalue-
Rvalue- C++ , , rvalue-. rvalue- T
T&&
.
:
class Int { int m_Value; public: Int(int val) : m_Value(val) {} int Get() const { return m_Value; } void Set(int val) { m_Value = val; } };
, rvalue- .
Int&& r0;
rvalue- ++ , lvalue. Un ejemplo:
Int i(7); Int&& r1 = i;
rvalue:
Int&& r2 = Int(42);
lvalue rvalue-:
Int&& r4 = static_cast<Int&&>(i);
rvalue- ( ) std::move()
, ( <utility>
).
Rvalue rvalue , .
int&& r5 = 2 * 2;
rvalue- .
Int&& r = 7; std::cout << r.Get() << '\n';
Rvalue- .
Int&& r = 5; Int& x = r;
Rvalue- , . , rvalue-, rvalue .
void Foo(Int&&); Int i(7); Foo(i);
, rvalue rvalue- , . rvalue-.
, , , rvalue-, (ambiguous) rvalue .
void Foo(Int&&); void Foo(const Int&);
Int i(7); Foo(i);
: rvalue- lvalue.
Int&& r = 7; Foo(r);
, rvalue-, lvalue std::move()
. . 2.4.
++11, rvalue- — -. (lvalue/rvalue) this
.
class X { public: X(); void DoIt() &;
.
, ( std::string
, std::vector<>
, etc.) . — . , rvalue- . , , - , - , . , , , rvalue, lvalue. , rvalue. . , ( lvalue), RVO.
Referencias
[Alexandrescu]
, . C++.: . del ingles - M .: LLC "I.D. », 2002.
[Guntheroth]
, . C++. .: . del ingles — .: «-», 2017.
[Josuttis]
, . C++: , 2- .: . del ingles - M .: LLC "I.D. », 2014.
[Dewhurst1]
, . C++. , 2- .: . del ingles — .: -, 2013.
[Dewhurst2]
, . C++. .: . del ingles — .: , 2012.
[Meyers1]
, . C++. 35 .: . del ingles — .: , 2000.
[Meyers2]
, . C++. 55 .: . del ingles — .: , 2014.
[Meyers3]
, . C++: 42 C++11 C ++14.: . del ingles - M .: LLC "I.D. », 2016.
[Sutter]
, . C++.: . del ingles — : «.. », 2015.
[Sutter/Alexandrescu]
, . , . ++.: . del ingles - M .: LLC "I.D. », 2015.
[Schildt]
, . C++.: . del ingles — .: -, 2005.
[Alger]
, . C++: .: . del ingles — .: « «», 1999.