studio reales proyectos programacion libro introducción incluye herramientas fundamentos fuente español código con avanzado aplicaciones c++ operator-overloading new-operator c++-faq delete-operator

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

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 es new_handler por la definición estándar predeterminada de operator 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.

Continuación 1


Parte II

... continuado

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 del new_handler utilizando la función set_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 clase std::bad_alloc ( 18.4.2.1 ) o una clase derivada de std::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 } }

Continuación 2


Parte III

... continuado

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 cualquier operator new(size_t) u operator new(size_t, const std::nothrow_t&) en la biblioteca estándar , y el valor proporcionado al operator delete[](void*) en la biblioteca estándar será uno de los valores devueltos por una invocación previa de cualquier operator new[](size_t) u operator 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; } };