Sobrecarga en C ++. Parte III Sobrecarga de declaraciones nuevas / eliminar


Continuamos la serie "C ++, cavando en profundidad". El propósito de esta serie es informar lo más posible sobre las diversas características del lenguaje, posiblemente bastante especial. Este artículo trata sobre la sobrecarga del operador new/delete. Este es el tercer artículo de la serie, el primero dedicado a las funciones y plantillas de sobrecarga, ubicado aquí , el segundo dedicado a los operadores de sobrecarga, ubicado aquí . Este artículo concluye una serie de tres artículos sobre sobrecarga en C ++.


Tabla de contenido


Tabla de contenido
  1. new/delete
    1.1.
    1.2.
    1.3.
  2. new/delete
    2.1.
    2.2.
      2.2.1. new/delete
      2.2.2. new/delete
      2.2.3.
      2.2.4.
      2.2.5. operator delete()
  3. new/delete
  4.
  5. -
  

1. Formas estándar de operadores nuevos / eliminados


C ++ admite varias opciones de operador new/delete. Se pueden dividir en estándar básico, estándar adicional y personalizado. Esta sección y la sección 2 analizan los formularios estándar; los formularios personalizados se analizarán en la sección 3.

1.1. Formularios estándar básicos


Las principales formas estándar de operadores new/deleteutilizadas al crear y eliminar un objeto y una matriz del tipo son las Tsiguientes:

new T(/*   */)
new T[/*   */]
delete ptr;
delete[] ptr;

Su trabajo se puede describir de la siguiente manera. Cuando se llama al operador new, la memoria se asigna primero al objeto. Si la selección es exitosa, se llama al constructor. Si el constructor produce una excepción, la memoria asignada se libera. Cuando se llama al operador, deletetodo sucede en el orden inverso: primero, se llama al destructor, luego se libera la memoria. El destructor no debe lanzar excepciones.

Cuando el operadornew[]utilizado para crear una matriz de objetos, la memoria se asigna primero para toda la matriz. Si la selección es exitosa, se llama al constructor predeterminado (u otro constructor, si hay un inicializador) para cada elemento de la matriz comenzando desde cero. Si algún constructor arroja una excepción, entonces, para todos los elementos creados de la matriz, se llama al destructor en el orden inverso de la llamada del constructor, luego se libera la memoria asignada. Para eliminar una matriz, debe llamar al operador delete[], y para todos los elementos de la matriz, se llama al destructor en el orden inverso del constructor, luego se libera la memoria asignada.

¡Atención! Es necesario llamar a la forma correcta del operadordeletedependiendo de si se elimina un solo objeto o una matriz. Esta regla debe observarse estrictamente, de lo contrario, puede obtener un comportamiento indefinido, es decir, puede suceder cualquier cosa: pérdidas de memoria, bloqueo, etc. Ver [Meyers1] para más detalles.

En la descripción anterior, es necesaria una aclaración. Para los llamados tipos triviales (tipos incorporados, estructuras de estilo C), el constructor predeterminado puede no ser llamado, y el destructor no hace nada en ningún caso.

Las funciones de asignación de memoria estándar, cuando no es posible satisfacer la solicitud, lanzan una excepción de tipo std::bad_alloc. Pero esta excepción puede ser detectada, para esto necesita instalar un interceptor global usando una llamada de función set_new_handler(), para más detalles vea [Meyers1].

Cualquier forma de operadordeleteaplicar de forma segura al puntero nulo.

Al crear una matriz con un operador, el new[]tamaño se puede establecer en cero.

Ambas formas del operador newpermiten el uso de inicializadores en llaves.

new int{42}
new int[8]{1,2,3,4}

1.2. Formularios estándar adicionales


Al conectar el archivo de encabezado <new>, quedan disponibles 4 formularios de operador estándar más new:

new(ptr) T(/*  */);
new(ptr) T[/*   */];
new(std::nothrow) T(/*   */);
new(std::nothrow) T[/*   */];

Los primeros dos de ellos se denominan newcolocación no asignada new. Un argumento ptres un puntero a una región de memoria que es lo suficientemente grande como para contener una instancia o matriz. Además, el área de memoria debe tener una alineación adecuada. Esta versión del operador newno asigna memoria; solo proporciona una llamada al constructor. Por lo tanto, esta opción le permite separar las fases de asignación de memoria e inicialización de objetos. Esta característica se usa activamente en contenedores estándar. Por deletesupuesto, no se puede llamar al operador para los objetos creados de esta manera. Para eliminar un objeto, debe llamar directamente al destructor y luego liberar la memoria de una manera que dependa del método de asignación de memoria.

Las dos newúltimas opciones se denominan el operador que no lanza excepciones (nothrow new) y difieren en que si es imposible satisfacer la solicitud, regresan nullptr, pero no lanzan una excepción de tipo std::bad_alloc. La eliminación de un objeto ocurre usando el operador principal delete. Estas opciones se consideran obsoletas y no se recomiendan para su uso.

1.3. Asignación de memoria y funciones libres


Las formas estándar de operadores new/deleteutilizan las siguientes funciones de asignación y desasignación:

void* operator new(std::size_t size);
void operator delete(void* ptr);
void* operator new[](std::size_t size);
void operator delete[](void* ptr);
void* operator new(std::size_t size, void* ptr);
void* operator new[](std::size_t size, void* ptr);
void* operator new(std::size_t size, const std::nothrow_t& nth);
void* operator new[](std::size_t size, const std::nothrow_t& nth);

Estas funciones se definen en el espacio de nombres global. Las funciones de asignación de memoria para las declaraciones de host newno hacen nada y simplemente regresan ptr.

C ++ 17 admite formas adicionales de asignación de memoria y funciones de desasignación, lo que indica la alineación. Éstos son algunos de ellos:

void* operator new (std::size_t size, std::align_val_t al);
void* operator new[](std::size_t size, std::align_val_t al);

Estos formularios no son directamente accesibles para el usuario, son utilizados por el compilador para objetos cuyos requisitos de alineación son superiores __STDCPP_DEFAULT_NEW_ALIGNMENT__, por lo que el problema principal es que el usuario no los oculta accidentalmente (ver sección 2.2.1). Recuerde que en C ++ 11 se hizo posible establecer explícitamente la alineación de los tipos de usuario.

struct alignas(32) X { /* ... */ };

2. Sobrecargar las formas estándar de operadores nuevos / eliminar


La sobrecarga de las formas estándar de operadores new/deleteconsiste en definir funciones definidas por el usuario para asignar y liberar memoria cuyas firmas coinciden con las estándar. Estas funciones se pueden definir en el espacio de nombres global o en una clase, pero no en un espacio de nombres que no sea global. La función de asignación de memoria para una declaración de host estándar newno se puede definir en el espacio de nombres global. Después de tal definición, los operadores correspondientes los new/deleteusarán, no los estándares.

2.1. Sobrecarga en el espacio de nombres global


Supongamos, por ejemplo, en un módulo en un espacio de nombres global que se definen funciones definidas por el usuario:

void* operator new(std::size_t size)
{
// ...
}

void operator delete(void* ptr)
{
// ...
}

En este caso, en realidad habrá un reemplazo (reemplazo) de las funciones estándar para asignar y liberar memoria para todas las llamadas del operador new/deletepara cualquier clase (incluidas las estándar) en todo el módulo. Esto puede conducir al caos completo. Tenga en cuenta que el mecanismo de sustitución descrito es un mecanismo especial implementado solo para este caso, y no un mecanismo general de C ++. En este caso, cuando se implementan las funciones del usuario para asignar y liberar memoria, se hace imposible llamar a las funciones estándar correspondientes, están completamente ocultas (el operador ::no ayuda), y cuando intenta llamarlas, se produce una llamada recursiva a la función del usuario.

Función definida de espacio de nombres global

void* operator new(std::size_t size, const std::nothrow_t& nth)
{
// ...
}

También reemplazará al estándar, pero habrá menos problemas potenciales, porque el operador que no arroja excepciones newrara vez se usa. Pero el formulario estándar tampoco está disponible.

La misma situación con funciones para matrices. Se desaconseja encarecidamente las

declaraciones de sobrecarga new/deleteen el espacio de nombres global.

2.2. Sobrecarga de clase


La sobrecarga de operadores new/deleteen una clase carece de las desventajas descritas anteriormente. La sobrecarga solo es efectiva al crear y eliminar instancias de la clase correspondiente, independientemente del contexto de invocación de operadores new/delete. Al implementar funciones definidas por el usuario para asignar y liberar memoria utilizando el operador, ::puede acceder a las funciones estándar correspondientes. Considera un ejemplo.

class X
{
// ...
public:
    void* operator new(std::size_t size)
    {
        std::cout << "X new\n";
        return ::operator new(size);
    }

    void operator delete(void* ptr)
    {
        std::cout << "X delete\n";
        ::operator delete(ptr);
    }

    void* operator new[](std::size_t size)
    {
        std::cout << "X new[]\n";
        return ::operator new[](size);
    }

    void operator delete[](void* ptr)
    {
        std::cout << "X delete[]\n";
        ::operator delete[](ptr);
    }
};

En este ejemplo, el seguimiento simplemente se agrega a las operaciones estándar. Ahora, en términos new X()y new X[N]utilizará estas funciones para asignar y liberar memoria.

Estas funciones son formalmente estáticas y se pueden declarar como static. Pero esencialmente son instancias, con la llamada a la función operator new(), comienza la creación de la instancia y la llamada de la función operator delete()completa su eliminación. Estas funciones nunca se requieren para otras tareas. Además, como se mostrará a continuación, la función operator delete()es esencialmente virtual. Por lo tanto, es más correcto declararlos sin ellos static.

2.2.1. Acceso a formas estándar de operadores nuevos / eliminados


Los operadores new/deletese pueden usar con un operador de resolución de alcance adicional, por ejemplo ::new(p) X(). En este caso, la función operator new()definida en la clase se ignorará y se utilizará el estándar correspondiente. Del mismo modo, puede usar el operador delete.

2.2.2. Ocultar otras formas de operadores nuevos / eliminar


Si ahora para la clase Xintentamos usar lanzar o no lanzar excepciones new, obtenemos un error. El hecho es que la función operator new(std::size_t size)ocultará otras formas operator new(). El problema se puede resolver de dos maneras. En el primero, debe agregar las opciones apropiadas a la clase (estas opciones simplemente deben delegar el funcionamiento de la función estándar). En el segundo, debe utilizar un operador newcon un operador de resolución de alcance, por ejemplo ::new(p) X().

2.2.3. Contenedores estándar


Si intentamos colocar las instancias Xen algún contenedor estándar, por ejemplo std::vector<X>, veremos que nuestras funciones no se utilizan para asignar y liberar memoria. El hecho es que todos los contenedores estándar tienen su propio mecanismo para asignar y liberar memoria (una clase especial de asignador, que es un parámetro de plantilla del contenedor), y utilizan un operador de colocación para inicializar los elementos new.

2.2.4. Herencia


Se heredan las funciones para asignar y liberar memoria. Si estas funciones se definen en la clase base, pero no en la derivada, los operadores se sobrecargarán para la clase derivada new/deletey las funciones definidas y asignadas en la clase base se utilizarán para asignar y liberar memoria.

Ahora considere una jerarquía de clases polimórficas, donde cada clase sobrecarga a los operadores new/delete. Ahora, deje que la instancia de la clase derivada se elimine utilizando el operador a deletetravés de un puntero a la clase base. Si el destructor de la clase base es virtual, entonces el estándar garantiza que se llama al destructor de esta clase derivada. En este caso operator delete(), la llamada a la función definida para esta clase derivada también está garantizada . Por lo tanto, la función es operator delete()realmente virtual.

2.2.5. Forma alternativa de la función delete () del operador


En una clase (especialmente cuando se usa la herencia), a veces es conveniente usar una forma alternativa de la función para liberar memoria:

void operator delete(void* p, std::size_t size);
void operator delete[](void* p, std::size_t size);

El parámetro sizeestablece el tamaño del elemento (incluso en la versión de la matriz). Este formulario le permite usar diferentes funciones para asignar y liberar memoria, dependiendo de la clase derivada específica.

3. Operadores de usuario nuevos / eliminar


C ++ puede admitir formas de operador personalizadas de la newsiguiente forma:

new(/*  */) T(/*   */)
new(/*  */) T[/*   */]

Para que estos formularios sean compatibles, es necesario determinar las funciones apropiadas para asignar y liberar memoria:

void* operator new(std::size_t size, /* .  */);
void* operator new[](std::size_t size, /* .  */);
void operator delete(void* p, /* .  */);
void operator delete[](void* p, /* .  */);

La lista de parámetros adicionales de las funciones de asignación de memoria no debe estar vacía y no debe consistir en una void*o const std::nothrow_t&, es decir, su firma no debe coincidir con una de las estándar. Listas de parámetros adicionales en operator new()y operator delete()deben coincidir. Los argumentos pasados ​​al operador newdeben corresponder a parámetros adicionales de las funciones de asignación de memoria. Una función personalizada operator delete()también puede tener el formato con un parámetro de tamaño opcional.

Estas funciones se pueden definir en el espacio de nombres global o en una clase, pero no en un espacio de nombres que no sea global. Si se definen en el espacio de nombres global, no reemplazan, sino que sobrecargan, las funciones estándar de asignación y liberación de memoria, por lo que su uso es predecible y seguro, y las funciones estándar siempre están disponibles. Si se definen en la clase, ocultan los formularios estándar, pero se puede obtener acceso a los formularios estándar utilizando el operador ::, esto se describe en detalle en la sección 2.2.

Los formularios de operador definidos por el usuario newse denominan newubicación definida por el usuario new. No deben confundirse con el operador de colocación estándar (sin asignación) newdescrito en la sección 1.2.

El formulario de operador correspondiente deleteno existe. Hay newdos formas de eliminar un objeto creado utilizando un operador definido por el usuario . Si una función definida por el usuario operator new()delega una operación de asignación de memoria a funciones de asignación de memoria estándar, entonces se puede usar un operador estándar delete. De lo contrario, deberá llamar explícitamente al destructor y luego a la función definida por el usuario operator delete(). El compilador llama a la función definida por el usuario en operator delete()un solo caso: cuando el newconstructor lanza una excepción durante la operación del operador definido por el usuario .

Aquí hay un ejemplo (en alcance global).

void* operator new(std::size_t size, int a, const char* b)
{
    std::cout << "new " << a << " + " << b << "\n";
    return ::operator new(size);
}
void operator delete(void* p, int a, const char* b)
{
    std::cout << "delete " << a << " + " << b << "\n";
    ::operator delete(p);
}

class X {/* ... */};
X* p = new(42, "meow") X(); // : new 42 + meow
delete p; //   ::operator delete()

4. Definición de funciones de asignación de memoria.


En estos ejemplos, las funciones de usuario operator new()y la operator delete()operación delegada correspondiente a las funciones estándar. A veces, esta opción es útil, pero el objetivo principal de la sobrecarga new/deletees crear un nuevo mecanismo para asignar / liberar memoria. La tarea no es simple, y antes de emprenderla, uno debe pensar cuidadosamente en todo. Scott Meyers [Meyers1] analiza los posibles motivos para tomar tal decisión (por supuesto, el principal es la eficiencia). También analiza los principales problemas técnicos asociados con la implementación correcta de las funciones definidas por el usuario para asignar y liberar memoria (utilizando la funciónset_new_handler(), sincronización multiproceso, alineación). Guntheroth proporciona un ejemplo de la implementación de funciones relativamente simples de asignación de memoria y desasignación definidas por el usuario. Antes de crear su propia versión, debe buscar soluciones ya preparadas, por ejemplo, puede traer la biblioteca Pool del proyecto Boost.

5. Clases de asignación de contenedores estándar


Como se mencionó anteriormente, los contenedores estándar utilizan clases especiales de asignación para asignar y liberar memoria. Estas clases son parámetros de plantilla de contenedores y el usuario puede definir su versión de dicha clase. Los motivos para tal solución son aproximadamente los mismos que para los operadores de sobrecarga new/delete. [Guntheroth] describe cómo crear tales clases.

Bibliografía


[Guntheroth]
Gunteroth, Kurt. Optimización de programas en C ++. Métodos probados para aumentar la productividad.: Por. De inglés - SPb.: Alpha-book LLC, 2017.
[Meyers1]
Meyers, Scott. Uso efectivo de C ++. 55 formas seguras de mejorar la estructura y el código de sus programas.: Per. De inglés - M .: DMK Press, 2014.

All Articles