Usos de destructor = eliminar;

10 minutos de lectura

avatar de usuario
skypjack

Considere la siguiente clase:

struct S { ~S() = delete; };

En breve y para el propósito de la pregunta: no puedo crear instancias de S me gusta S s{}; porque no pude destruirlos.
Como se menciona en los comentarios, todavía puedo crear una instancia haciendo S *s = new S;pero no puedo borrarlo también.
Por lo tanto, el único uso que puedo ver para un destructor eliminado es algo como esto:

struct S {
    ~S() = delete;
    static void f() { }
};

int main() {
    S::f();
}

Es decir, defina una clase que exponga solo un montón de funciones estáticas y prohíba cualquier intento de crear una instancia de esa clase.

¿Cuáles son los otros usos (si los hay) de un destructor eliminado?

  • por supuesto, puede crear una instancia de S en este caso, solo use new S

    – manzana manzana

    22 de noviembre de 2016 a las 13:21

  • @skypjack Puede crear (y eliminar) usando la ubicación nueva incluso si se elimina el destructor. Tal vez esto podría tener algunos usos donde desea que un objeto global administre diferentes subobjetos y no desea que estos subobjetos vivan fuera del contenedor …

    – Holt

    22 de noviembre de 2016 a las 13:26

  • Es decir, defina una clase que exponga solo un montón de funciones estáticas y prohíba cualquier intento de crear una instancia de esa clase. también conocido como un espacio de nombres?

    – Borglíder

    22 de noviembre de 2016 a las 13:26

  • @Borgleader El hecho de que pueda hacerlo de otra manera, ¿cómo puede ayudar aquí? No estoy pidiendo enfoques alternativos, ya los conozco, solo tengo curiosidad por saber cuáles son los usos de destructor = delete;.

    – skypjack

    22 de noviembre de 2016 a las 13:28

  • Los objetos @Borgleader con métodos estáticos se pueden usar como plantilla, los espacios de nombres no.

    –Nir Friedman

    22 de noviembre de 2016 a las 14:12

avatar de usuario
Yakk – Adam Nevraumont

Si tienes un objeto que nunca, nunca debería ser deleted o almacenado en la pila (almacenamiento automático), o almacenado como parte de otro objeto, =delete prevendrá todo esto.

struct Handle {
  ~Handle()=delete;
};

struct Data {
  std::array<char,1024> buffer;
};

struct Bundle: Handle {
  Data data;
};

using bundle_storage = std::aligned_storage_t<sizeof(Bundle), alignof(Bundle)>;

std::size_t bundle_count = 0;
std::array< bundle_storage, 1000 > global_bundles;

Handle* get_bundle() {
  return new ((void*)global_bundles[bundle_count++]) Bundle();
}
void return_bundle( Handle* h ) {
  Assert( h == (void*)global_bundles[bundle_count-1] );
  --bundle_count;
}
char get_char( Handle const* h, std::size_t i ) {
  return static_cast<Bundle*>(h).data[i];
}
void set_char( Handle const* h, std::size_t i, char c ) {
  static_cast<Bundle*>(h).data[i] = c;
}

Aquí tenemos opaco Handles que no pueden declararse en la pila ni asignarse dinámicamente. Tenemos un sistema para obtenerlos de una matriz conocida.

Creo que nada de lo anterior es un comportamiento indefinido; no poder destruir un Bundle es aceptable, como lo es crear uno nuevo en su lugar.

Y la interfaz no tiene que exponer cómo Bundle obras. Solo un opaco Handle.

Ahora, esta técnica puede ser útil si otras partes del código necesitan saber que todos los identificadores están en ese búfer específico, o si se realiza un seguimiento de su vida útil de formas específicas. Posiblemente esto también podría manejarse con constructores privados y funciones de fábrica amigas.

  • Creo que hacer que la duración del almacenamiento automático (y, creo, estático) sea imposible sin hacer cumplir una fábrica (como sería el caso con un ctor no público) es lo más destacado aquí.

    – Peter – Reincorporar a Mónica

    22 de noviembre de 2016 a las 16:50


  • ~Handle=delete(); debiera ser ~Handle()=delete; ¿seguramente?

    – usuario253751

    22 de noviembre de 2016 a las 21:41

  • @immibis que, =delete no conmuta con ()?

    – Yakk – Adam Nevraumont

    23 de noviembre de 2016 a las 0:03

avatar de usuario
Domso

un escenario podría ser la prevención de desasignaciones incorrectas:

#include <stdlib.h>

struct S {
    ~S() = delete;
};


int main() {

    S* obj= (S*) malloc(sizeof(S));

    // correct
    free(obj);

    // error
    delete obj;

    return 0;

}

esto es muy rudimentario, pero se aplica a cualquier proceso especial de asignación/desasignación (por ejemplo, una fábrica)

un ejemplo más estilo ‘c++’

struct data {
    //...
};

struct data_protected {
    ~data_protected() = delete;
    data d;
};

struct data_factory {


    ~data_factory() {
        for (data* d : data_container) {
            // this is safe, because no one can call 'delete' on d
            delete d;
        }
    }

    data_protected* createData() {
        data* d = new data();
        data_container.push_back(d);
        return (data_protected*)d;
    }



    std::vector<data*> data_container;
};

  • Llamar a este enfoque “correcto” es un poco extraño. Por lo general, trataría de evitar malloc y free en C++.

    – Carreras de ligereza en órbita

    22 de noviembre de 2016 a las 15:11

  • Supongo que la intención aquí es usar alguna forma personalizada de asignar y desasignar (fábrica) en lugar de new y delete. Probablemente sería más claro con nombres como my_alloc y my_dealloc; los nombres malloc y free son solo un muy mal ejemplo. También, my_alloc debe devolver un puntero adecuado (no void*).

    – anatolyg

    22 de noviembre de 2016 a las 15:45

  • Realmente todavía no veo esto como C ++ adecuado

    –Bruno Ferreira

    22 de noviembre de 2016 a las 19:18

  • malloc posiblemente podría aparecer en un entorno mixto… He añadido un mejor ejemplo

    – Domso

    22 de noviembre de 2016 a las 20:14

  • He votado con la advertencia de que se usaría una mejor implementación de C++ 11 std::unique_ptr<> para lograr el destructor de fábrica, pero hacerlo oscurecería la mecánica que se está demostrando. Entonces es un buen ejemplo pero no la implementación correcta.

    – perseverancia

    23 de noviembre de 2016 a las 11:42

¿Por qué marcar un destructor como delete?

Para evitar que se invoque al destructor, por supuesto 😉

¿Cuáles son los casos de uso?

puedo ver al menos 3 usos diferentes:

  1. La clase nunca debe ser instanciada; en este caso, también esperaría un constructor predeterminado eliminado.
  2. Debería filtrarse una instancia de esta clase; por ejemplo, una instancia de registro único
  3. Una instancia de esta clase solo se puede crear y eliminar mediante un mecanismo específico; esto podría ocurrir notablemente cuando se usa FFI

Para ilustrar el último punto, imagine una interfaz C:

struct Handle { /**/ };

Handle* xyz_create();
void xyz_dispose(Handle*);

En C++, querrías envolverlo en un unique_ptr para automatizar el lanzamiento, pero ¿qué pasa si accidentalmente escribes: unique_ptr<Handle>? ¡Es un desastre en tiempo de ejecución!

Entonces, en su lugar, puede modificar la definición de clase:

struct Handle { /**/ ~Handle() = delete; };

y luego el compilador se atragantará unique_ptr<Handle> obligándote a usar correctamente unique_ptr<Handle, xyz_dispose> en cambio.

  • ¿Es realmente un desastre escribir unique_ptr<Handle>? Siempre y cuando el unique_ptr se construye con el borrador válido xyz_dispose en xyz_createuna vez devuelto unique_ptr<Handle, xyz_dispose> se moverá construido en unique_ptr<Handle> con el eliminador construido a partir de xyz_dispose¿no es así?

    – contrato

    22 de noviembre de 2016 a las 16:05

  • @wasthishelpful: No; diferente a shared_ptr qué tipo-borrar el destructor del tipo, unique_ptr no tiene indirección. Además, tenga en cuenta que xyz_create siendo una función C, devuelve un hueso desnudo Handle*No un unique_ptr. Finalmente, esperaría un error del compilador si uno intenta convertir de unique_ptr<Handle, xyz_dispose> a unique_ptr<Handle>.

    – Matthieu M.

    22 de noviembre de 2016 a las 16:09

  • Si está ajustando la definición (presumiblemente en un #ifdef), ¿por qué no estás escribiendo ~Handle() {xyz_dispose(this);} en lugar de eliminar el destructor? Ahora, unique_ptr<Handle> hace lo sensato automáticamente, en lugar de hacer que escriba el triturador en cada uso del tipo (que puede ser typedefed alrededor, pero aún así…)

    – LThodo

    22 de noviembre de 2016 a las 16:23

  • @LThode: Yo personalmente escribiría una clase sobre std::unique_ptr<Handle, xyz_dispose>facilitaría el uso (que llamar repetidamente .get() para obtener acceso al puntero sin procesar) y proporcionar encapsulación (el cliente de la clase estaría aislado de los cambios en la interfaz C). Es una elección personal 🙂 De todos modos, no estoy diciendo que esos ejemplos sean geniales; Estoy diciendo que son posibles. Solo he usado esto en el caso (1) personalmente.

    – Matthieu M.

    22 de noviembre de 2016 a las 16:36

  • ¿Cómo una interfaz C puede devolver una clase con destructor eliminado? Y usar una definición diferente para esa clase según el idioma parece inseguro y no portátil.

    – Jarod42

    22/11/2016 a las 19:00

avatar de usuario
perseverancia

Hay dos casos de uso plausibles. Primero (como señalan algunos comentarios) podría ser aceptable asignar objetos dinámicamente, fallar en delete y permitir que el sistema operativo se limpie al final del programa.

Alternativamente (y aún más extraño) puede asignar un búfer y crear un objeto en él y luego eliminar el búfer para recuperar el lugar, pero nunca intente llamar al destructor.

#include <iostream>

struct S { 
    const char* mx;

    const char* getx(){return mx;}

    S(const char* px) : mx(px) {}
    ~S() = delete; 
};

int main() {
    char *buffer=new char[sizeof(S)];
    S *s=new(buffer) S("not deleting this...");//Constructs an object of type S in the buffer.
    //Code that uses s...
    std::cout<<s->getx()<<std::endl;

    delete[] buffer;//release memory without requiring destructor call...
    return 0;
}

Ninguno de estos parece una buena idea excepto en circunstancias especiales. Si el destructor creado automáticamente no hace nada (porque el destructor de todos los miembros es trivial), el compilador creará un destructor sin efecto.

Si el destructor creado automáticamente hiciera algo no trivial, es muy probable que comprometa la validez de su programa al no ejecutar su semántica.

Dejar salir un programa main() y permitir que el entorno se ‘limpie’ es una técnica válida, pero es mejor evitarla a menos que las limitaciones lo hagan estrictamente necesario. ¡En el mejor de los casos, es una excelente manera de enmascarar fugas de memoria genuinas!

Sospecho que la función está presente para completar con la capacidad de delete otros miembros generados automáticamente.

Me encantaría ver un uso práctico real de esta capacidad.

Existe la noción de una clase estática (sin constructores) y, por lo tanto, lógicamente no requiere destructor. Pero tales clases se implementan más apropiadamente como un namespace no tienen un (buen) lugar en C++ moderno a menos que tengan una plantilla.

Crear una instancia de un objeto con new y no borrarlo nunca es la forma más segura de implementar un Singleton de C++, porque evita todos y cada uno de los problemas de orden de destrucción. Un ejemplo típico de este problema sería un Singleton “Logging” al que se accede en el destructor de otra clase Singleton. Alexandrescu una vez dedicó una sección entera en su clásico “Diseño C++ moderno” libro sobre formas de hacer frente a problemas de orden de destrucción en implementaciones Singleton.

Es bueno tener un destructor eliminado para que incluso la propia clase Singleton no pueda eliminar accidentalmente la instancia. También evita el uso loco como delete &SingletonClass::Instance() (si Instance() devuelve una referencia, como debería; no hay ninguna razón para que devuelva un puntero).

Sin embargo, al final del día, nada de esto es realmente digno de mención. Y, por supuesto, no deberías usar Singletons en primer lugar de todos modos.

  • ¿Cómo puede ser la forma más segura de crear un singleton si puede crear un segundo cuando lo desee?

    – rubenvb

    22/11/2016 a las 18:50

  • @rubenvb: Al hacer que el constructor sea privado. De lo contrario, no es un Singleton. Pero ese es un detalle tan trivial que ni siquiera lo mencioné.

    – Christian Hackel

    22 de noviembre de 2016 a las 20:26

  • ¿Cómo puede ser la forma más segura de crear un singleton si puede crear un segundo cuando lo desee?

    – rubenvb

    22/11/2016 a las 18:50

  • @rubenvb: Al hacer que el constructor sea privado. De lo contrario, no es un Singleton. Pero ese es un detalle tan trivial que ni siquiera lo mencioné.

    – Christian Hackel

    22 de noviembre de 2016 a las 20:26

¿Ha sido útil esta solución?