46 votes

Utilisations de destructeur = supprimer ;

Considérons la classe suivante :

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

Brièvement et dans le but de la question : Je ne peux pas créer d'instances de S comme S s{}; car je n'ai pas pu les détruire.
Comme mentionné dans les commentaires, je peux toujours créer une instance en faisant S *s = new S; mais je ne peux pas non plus le supprimer.
Par conséquent, la seule utilisation que je puisse voir pour un destructeur supprimé est quelque chose comme ceci :

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

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

C'est-à-dire définir une classe qui n'expose qu'un ensemble de fonctions statiques et interdire toute tentative de créer une instance de cette classe.

Quelles sont les autres utilisations (le cas échéant) d'un destructeur supprimé ?

22voto

Yakk Points 31636

Si vous avez un objet qui ne doit jamais, jamais être delete d ou stocké sur la pile (stockage automatique), ou stocké comme partie d'un autre objet, =delete permettra d'éviter tout cela.

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;
}

Ici, nous avons l'opaque Handle qui ne peuvent pas être déclarés sur la pile ni alloués dynamiquement. Nous avons un système pour les récupérer à partir d'un tableau connu.

Je crois que rien de ce qui précède n'est un comportement indéfini ; échouer à détruire une Bundle est acceptable, tout comme la création d'un nouveau à sa place.

Et l'interface n'a pas besoin d'exposer comment Bundle travaux. Juste un opaque Handle .

Cette technique peut être utile si d'autres parties du code ont besoin de savoir que tous les Handles se trouvent dans ce tampon spécifique, ou si leur durée de vie est suivie de manière spécifique. Il est possible que cela puisse également être géré avec des constructeurs privés et des fonctions d'usine amies.

16voto

Domso Points 745

un scénario pourrait être la prévention d'une mauvaise désallocation :

#include <stdlib.h>

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

int main() {

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

    // correct
    free(obj);

    // error
    delete obj;

    return 0;

}

Ceci est très rudimentaire, mais s'applique à tout processus spécial d'allocation/désallocation (par exemple une usine).

un exemple plus "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;
};

9voto

Matthieu M. Points 101624

Pourquoi marquer un destructeur comme delete ?

Pour éviter que le destructeur ne soit invoqué, bien sûr ;)

Quels sont les cas d'utilisation ?

Je peux voir au moins 3 utilisations différentes :

  1. La classe ne devrait jamais être instanciée ; dans ce cas, je m'attendrais également à un constructeur par défaut supprimé.
  2. Une instance de cette classe doit fuir ; par exemple, une instance singleton de journalisation
  3. Une instance de cette classe ne peut être créée et éliminée que par un mécanisme spécifique ; cela peut notamment se produire lors de l'utilisation de FFI.

Pour illustrer ce dernier point, imaginez une interface C :

struct Handle { /**/ };

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

En C++, vous voudriez l'intégrer dans un fichier de type unique_ptr pour automatiser le lancement, mais que faire si vous écrivez accidentellement : unique_ptr<Handle> ? C'est un désastre pour l'exécution !

Donc, à la place, vous pouvez modifier la définition de la classe :

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

et alors le compilateur s'étouffera sur unique_ptr<Handle> vous obligeant à utiliser correctement unique_ptr<Handle, xyz_dispose> à la place.

7voto

Dan Allen Points 2417

Il existe deux cas d'utilisation plausibles. Tout d'abord (comme le notent certains commentaires), il pourrait être acceptable d'allouer dynamiquement des objets, de ne pas delete et permettre au système d'exploitation de faire le ménage à la fin du programme.

Alternativement (et encore plus bizarre), vous pourriez allouer un tampon et créer un objet dans celui-ci, puis supprimer le tampon pour récupérer la place mais sans jamais provoquer une tentative d'appel du destructeur.

#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;
}

Aucun d'entre eux ne semble être une bonne idée, sauf dans des circonstances particulières. Si le destructeur créé automatiquement ne fait rien (parce que le destructeur de tous les membres est trivial), le compilateur créera un destructeur sans effet.

Si le destructeur créé automatiquement fait quelque chose de non trivial, vous compromettez très probablement la validité de votre programme en n'exécutant pas sa sémantique.

Laisser partir un programme main() et permettre à l'environnement de "nettoyer" est une technique valable mais qu'il vaut mieux éviter à moins que des contraintes ne le rendent strictement nécessaire. Au mieux, c'est un excellent moyen de masquer les véritables fuites de mémoire !

Je soupçonne que la fonctionnalité est présente par souci d'exhaustivité avec la possibilité de delete d'autres membres générés automatiquement.

J'aimerais bien voir une utilisation pratique réelle de cette capacité.

Il y a la notion de classe statique (sans constructeur) et donc logiquement ne nécessitant pas de destructeur. Mais de telles classes sont plutôt implémentées en tant qu'objets de type namespace n'ont pas de (bonne) place dans le C++ moderne à moins d'être modélisées.

5voto

Christian Hackl Points 4763

Créer une instance d'un objet avec new et ne jamais le supprimer est la manière la plus sûre d'implémenter un Singleton C++, car elle évite tous les problèmes d'ordre de destruction. Un exemple typique de ce problème serait un Singleton "Logging" auquel on accède dans le destructeur d'une autre classe Singleton. Alexandrescu a consacré une section entière de son ouvrage classique intitulé "Conception moderne du C++" livre sur les moyens de faire face aux problèmes d'ordre de destruction dans les implémentations Singleton.

Il est bon d'avoir un destructeur supprimé afin que la classe Singleton elle-même ne puisse pas supprimer accidentellement l'instance. Cela permet également d'éviter des utilisations folles comme delete &SingletonClass::Instance() (si Instance() renvoie une référence, comme il se doit ; il n'y a aucune raison pour qu'il renvoie un pointeur).

En fin de compte, rien de tout cela n'est vraiment digne d'intérêt. Et bien sûr, vous ne devriez pas utiliser les singletons en premier lieu de toute façon.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X