reales - ¿Cómo debo escribir ISO C++ Estándar nuevo personalizado y eliminar operadores?
libro de android studio en español pdf (4)
¿Cómo debo escribir los operadores new
y delete
personalizados conforme a la norma ISO C ++?
Esto es a continuación de Sobrecargar nuevo y eliminar en las preguntas frecuentes sobre C ++ inmensamente iluminadoras, la sobrecarga del operador y su seguimiento. ¿Por qué debería uno reemplazar los operadores nuevos y eliminar por defecto?
Sección 1: Escribir un new
operador conforme a las normas
- Parte 1: Comprender los requisitos para escribir un
new
operador personalizado - Parte 2: comprender los requisitos
new_handler
- Parte 3: Comprender los requisitos de escenarios específicos
Sección 2: Escribir un operador de delete
conforme a las normas
(Nota: Esto debe ser una entrada a las preguntas frecuentes de C ++ de Stack Overflow . Si desea criticar la idea de proporcionar una pregunta frecuente en este formulario, entonces la publicación en meta que inició todo esto sería el lugar para hacerlo). esa pregunta se monitorea en la sala de chat de C ++ , donde la idea de las preguntas frecuentes comenzó en primer lugar, por lo que es muy probable que su respuesta sea leída por aquellos a quienes se les ocurrió la idea).
Nota: La respuesta se basa en los aprendizajes de C ++ más efectivo de Scott Meyers y el estándar ISO C ++.
Parte I
Esta entrada de C ++ FAQ explicó por qué uno querría sobrecargar new
operadores y delete
para su propia clase. La presente pregunta frecuente trata de explicar cómo se hace de una manera estándar.
Implementación de un new
operador personalizado
El estándar de C ++ (§18.4.1.1) define operator new
como:
void* operator new (std::size_t size) throw (std::bad_alloc);
El estándar C ++ especifica la semántica que deben cumplir las versiones personalizadas de estos operadores en §3.7.3 y §18.4.1
Vamos a resumir los requisitos.
Requisito n. ° 1: debe asignar dinámicamente al menos un size
bytes de memoria y devolver un puntero a la memoria asignada. Cita del estándar C ++, sección 3.7.4.1.3:
La función de asignación intenta asignar la cantidad de almacenamiento solicitada. Si tiene éxito, devolverá la dirección del inicio de un bloque de almacenamiento cuya longitud en bytes será al menos tan grande como el tamaño solicitado ...
El estándar además impone:
... El puntero devuelto se alineará adecuadamente para que se pueda convertir en un puntero de cualquier tipo de objeto completo y luego se use para acceder al objeto o matriz en el almacenamiento asignado (hasta que el almacenamiento sea desasignado explícitamente mediante una llamada al correspondiente función de desasignación). Incluso si el tamaño del espacio solicitado es cero, la solicitud puede fallar. Si la solicitud tiene éxito, el valor devuelto será un valor de puntero no nulo (4.10) p0 diferente de cualquier valor devuelto previamente p1, a menos que ese valor p1 se pase posteriormente a una
delete
operador.
Esto nos da más requisitos importantes:
Requisito n. ° 2: la función de asignación de memoria que usamos (generalmente malloc()
o algún otro asignador personalizado) debe devolver un puntero adecuadamente alineado a la memoria asignada, que puede convertirse en un puntero de un tipo de objeto completo y utilizarse para acceder al objeto .
Requisito n. ° 3: Nuestro operador personalizado new
debe devolver un puntero legítimo incluso cuando se soliciten cero bytes.
Uno de los requisitos evidentes que se pueden inferir del new
prototipo es:
Requisito n. ° 4: si el elemento new
no puede asignar memoria dinámica del tamaño solicitado, entonces debería arrojar una excepción del tipo std::bad_alloc
.
¡Pero! Hay más en eso de lo que parece: si observa más de cerca la new
documentation operador (la cita del estándar sigue más abajo), indica:
Si se ha utilizado set_new_handler para definir una función new_handler , esta función
new_handler
esnew_handler
por la definición estándar predeterminada deoperator new
si no puede asignar el almacenamiento solicitado por sí mismo.
Para comprender cómo nuestras new
necesidades personalizadas deben sustentar este requisito, debemos entender:
¿Qué es new_handler
y set_new_handler
?
new_handler
es un typedef para un puntero a una función que toma y no devuelve nada, y set_new_handler
es una función que toma y devuelve un new_handler
.
El parámetro set_new_handler
es un puntero al operador de función new debe llamar si no puede asignar la memoria solicitada. Su valor de retorno es un puntero a la función del controlador previamente registrado, o nulo si no había un controlador anterior.
Un momento oportuno para una muestra de código para aclarar las cosas:
#include <iostream>
#include <cstdlib>
// function to call if operator new can''t allocate enough memory or error arises
void outOfMemHandler()
{
std::cerr << "Unable to satisfy request for memory/n";
std::abort();
}
int main()
{
//set the new_handler
std::set_new_handler(outOfMemHandler);
//Request huge memory size, that will cause ::operator new to fail
int *pBigDataArray = new int[100000000L];
return 0;
}
En el ejemplo anterior, el operator new
(lo más probable) no podrá asignar espacio para 100 000 000 enteros, y se outOfMemHandler()
la función outOfMemHandler()
, y el programa se cancelará después de emitir un mensaje de error .
Es importante observar aquí que cuando el operator new
no puede cumplir con una solicitud de memoria, llama a la función de new-handler
repetidamente hasta que puede encontrar suficiente memoria o no hay más manejadores nuevos. En el ejemplo anterior, a menos que llamemos a std::abort()
, se outOfMemHandler()
repetidamente . Por lo tanto, el controlador debe asegurarse de que la siguiente asignación tenga éxito, o registrar otro controlador, o registrar ningún controlador, o no devolver (es decir, finalizar el programa). Si no hay un nuevo controlador y la asignación falla, el operador emitirá una excepción.
Parte II
Dado el comportamiento del operator new
del ejemplo, un new_handler
bien diseñado debe hacer una de las siguientes cosas:
Haga que haya más memoria disponible: esto puede permitir que el siguiente intento de asignación de memoria dentro del ciclo del operador nuevo tenga éxito. Una forma de implementar esto es asignar un gran bloque de memoria al inicio del programa, luego libérelo para usarlo en el programa la primera vez que se invoque al nuevo manejador.
Instale un nuevo controlador diferente: si el controlador nuevo actual no puede hacer que haya más memoria disponible, y si hay otro controlador nuevo que pueda, entonces el controlador nuevo actual puede instalar el otro controlador nuevo en su lugar ( llamando a set_new_handler
). La próxima vez que operator new llame a la función new-handler, obtendrá la última instalada.
(Una variación de este tema es que un controlador nuevo modifique su propio comportamiento, por lo que la próxima vez que se invoca, hace algo diferente. Una forma de lograr esto es hacer que el controlador nuevo modifique la estática, el espacio de nombre específico o datos globales que afectan el comportamiento del nuevo manejador).
Desinstale el controlador nuevo: esto se hace pasando un puntero nulo a set_new_handler
. Sin un controlador nuevo instalado, el operator new
arrojará una excepción ((convertible a) std::bad_alloc
) cuando la asignación de memoria no sea exitosa.
Lanza una excepción convertible a std::bad_alloc
. Tales excepciones no serán detectadas por el operator new
, sino que se propagarán al sitio que origina la solicitud de memoria.
No devuelto: llamando a abort
o exit
.
Para implementar un new_handler
específico de new_handler
debemos proporcionar una clase con sus propias versiones de set_new_handler
y operator new
. El set_new_handler
la clase permite a los clientes especificar el nuevo controlador para la clase (exactamente como el estándar set_new_handler
permite a los clientes especificar el nuevo controlador global). El operator new
la clase garantiza que se utilice el nuevo controlador específico de clase en lugar del nuevo controlador global cuando se asigna memoria para objetos de clase.
Ahora que entendemos mejor a new_handler
& set_new_handler
podemos modificar el Requisito # 4 adecuadamente como:
Requisito # 4 (Mejorado):
Nuestro operator new
debe tratar de asignar memoria más de una vez, llamando a la función de nuevo manejo después de cada falla. La suposición aquí es que la nueva función de manejo podría hacer algo para liberar algo de memoria. Solo cuando el puntero a la nueva función de manejo es null
, el operator new
lanza una excepción.
Como se prometió, la cita del Estándar:
Sección 3.7.4.1.3:
Una función de asignación que no puede asignar almacenamiento puede invocar a
new_handler
actualmente instalado (18.4.2.2
), si corresponde. [Nota: Una función de asignación suministrada por el programa puede obtener la dirección delnew_handler
utilizando la funciónset_new_handler
(18.4.2.3
).] Si una función de asignación declarada con una especificación de excepción vacía (15.4
),throw()
, falla asignar almacenamiento, devolverá un puntero nulo. Cualquier otra función de asignación que no pueda asignar almacenamiento solo deberá indicar la falla arrojando una excepción de la clasestd::bad_alloc
(18.4.2.1
) o una clase derivada destd::bad_alloc
.
Armado con los requisitos n . ° 4 , probemos el pseudo código para nuestro new operator
:
void * operator new(std::size_t size) throw(std::bad_alloc)
{
// custom operator new might take additional params(3.7.3.1.1)
using namespace std;
if (size == 0) // handle 0-byte requests
{
size = 1; // by treating them as
} // 1-byte requests
while (true)
{
//attempt to allocate size bytes;
//if (the allocation was successful)
//return (a pointer to the memory);
//allocation was unsuccessful; find out what the current new-handling function is (see below)
new_handler globalHandler = set_new_handler(0);
set_new_handler(globalHandler);
if (globalHandler) //If new_hander is registered call it
(*globalHandler)();
else
throw std::bad_alloc(); //No handler is registered throw an exception
}
}
Parte III
Tenga en cuenta que no podemos obtener el puntero de la función del nuevo controlador directamente, tenemos que llamar a set_new_handler
para averiguar qué es. Esto es crudo pero efectivo, al menos para el código de un solo subproceso. En un entorno multiproceso, probablemente sea necesario algún tipo de bloqueo para manipular de forma segura las estructuras de datos (globales) que se encuentran detrás de la función de nuevo manejo. ( Más citación / detalles son bienvenidos en esto. )
Además, tenemos un bucle infinito y la única forma de salir del bucle es que la memoria se asigne correctamente o que la nueva función de manejo haga una de las cosas que inferimos anteriormente. A menos que new_handler
haga una de esas cosas, este bucle dentro del new
operador nunca terminará.
Una advertencia: §3.7.4.1.3
cuenta que el estándar ( §3.7.4.1.3
, citado anteriormente) no dice explícitamente que el operador new
sobrecargado debe implementar un bucle infinito, sino que simplemente dice que ese es el comportamiento predeterminado. Entonces, este detalle está abierto a la interpretación, pero la mayoría de los compiladores ( GCC y Microsoft Visual C ++ ) implementan esta funcionalidad de bucle (puede compilar los ejemplos de código proporcionados anteriormente). Además, dado que una autorización de C ++ como Scott Meyers sugiere este enfoque, es bastante razonable.
Escenarios especiales
Consideremos el siguiente escenario.
class Base
{
public:
static void * operator new(std::size_t size) throw(std::bad_alloc);
};
class Derived: public Base
{
//Derived doesn''t declare operator new
};
int main()
{
// This calls Base::operator new!
Derived *p = new Derived;
return 0;
}
Como explica este FAQ, una razón común para escribir un administrador de memoria personalizado es optimizar la asignación para objetos de una clase específica, no para una clase o cualquiera de sus clases derivadas, lo que básicamente significa que nuestro operador nuevo para la clase Base es típicamente sintonizado para objetos de tamaño sizeof(Base)
nada más grande y nada más pequeño.
En la muestra anterior, debido a la herencia, la clase derivada Derived
hereda el nuevo operador de la clase Base. Esto hace que el operador que llama sea nuevo en una clase base para asignar memoria para un objeto de una clase derivada sea posible. La mejor manera para que nuestro operator new
maneje esta situación es desviar esas llamadas solicitando la cantidad "incorrecta" de memoria al operador estándar nuevo, de esta manera:
void * Base::operator new(std::size_t size) throw(std::bad_alloc)
{
if (size != sizeof(Base)) // If size is "wrong,", that is, != sizeof Base class
{
return ::operator new(size); // Let std::new handle this request
}
else
{
//Our implementation
}
}
Tenga en cuenta que el cheque para el tamaño también incorpora nuestro requisito n. ° 3 . Esto se debe a que todos los objetos independientes tienen un tamaño distinto de cero en C ++, por lo que sizeof(Base)
nunca puede ser cero, por lo que si el tamaño es cero, la solicitud se reenviará a ::operator new
y se garantiza que manejará de forma compatible estándar.
Cita: Del creador del propio C ++, el Dr. Bjarne Stroustrup.
Implementación de un operador de eliminación personalizado
La biblioteca de C ++ Standard ( §18.4.1.1
) define el operator delete
como:
void operator delete(void*) throw();
Permítanos repetir el ejercicio de reunir los requisitos para escribir nuestra operator delete
personalizado:
Requisito n. ° 1: Devolverá void
y su primer parámetro será void*
. Un delete operator
personalizada también puede tener más de un parámetro, pero bueno, solo necesitamos un parámetro para pasar el puntero apuntando a la memoria asignada.
Citación del Estándar C ++:
Sección §3.7.3.2.2:
"Cada función de desasignación volverá a ser nula y su primer parámetro será nulo *. Una función de desasignación puede tener más de un parámetro ....."
Requisito n. ° 2: debe garantizar que sea seguro eliminar un puntero nulo pasado como argumento.
Citación del Estándar C ++: Sección §3.7.3.2.3:
El valor del primer argumento proporcionado a una de las funciones de desasignación proporcionadas en la biblioteca estándar puede ser un valor de puntero nulo; si es así, la llamada a la función de desasignación no tiene ningún efecto. De lo contrario, el valor proporcionado al
operator delete(void*)
en la biblioteca estándar será uno de los valores devueltos por una invocación previa de cualquieroperator new(size_t)
uoperator new(size_t, const std::nothrow_t&)
en la biblioteca estándar , y el valor proporcionado aloperator delete[](void*)
en la biblioteca estándar será uno de los valores devueltos por una invocación previa de cualquieroperator new[](size_t)
uoperator new[](size_t, const std::nothrow_t&)
en la biblioteca estándar.
Requisito n.º 3: si el puntero que se pasa no es null
, el delete operator
debe desasignar la memoria dinámica asignada y asignada al puntero.
Citación del Estándar C ++: Sección §3.7.3.2.4:
Si el argumento dado a una función de desasignación en la biblioteca estándar es un puntero que no es el valor del puntero nulo (4.10), la función de desasignación desasignará el almacenamiento al que hace referencia el puntero, invalidando todos los punteros en referencia a cualquier parte del almacenamiento desasignado
Requisito n. ° 4: Además, dado que nuestro operador específico de clase envía nuevas solicitudes de reenvío del tamaño "incorrecto" a ::operator new
, DEBEMOS reenviar las solicitudes de eliminación de "tamaño incorrecto" a ::operator delete
.
Por lo tanto, de acuerdo con los requisitos que resumimos aquí arriba, se encuentra un pseudo código conforme estándar para un delete operator
personalizado:
class Base
{
public:
//Same as before
static void * operator new(std::size_t size) throw(std::bad_alloc);
//delete declaration
static void operator delete(void *rawMemory, std::size_t size) throw();
void Base::operator delete(void *rawMemory, std::size_t size) throw()
{
if (rawMemory == 0)
{
return; // No-Op is null pointer
}
if (size != sizeof(Base))
{
// if size is "wrong,"
::operator delete(rawMemory); //Delegate to std::delete
return;
}
//If we reach here means we have correct sized pointer for deallocation
//deallocate the memory pointed to by rawMemory;
return;
}
};