46 votes

Utilisations du destructeur = delete;

Considérez la classe suivante :

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

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

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

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

Autrement dit, définir une classe qui expose uniquement une série de fonctions statiques et interdire toute tentative de créer une instance de cette classe.

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

22voto

Yakk Points 31636

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

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

struct Data {
  std::array buffer;
};

struct Bundle: Handle {
  Data data;
};

using bundle_storage = std::aligned_storage_t;

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(h).data[i];
}
void set_char( Handle const* h, std::size_t i, char c ) {
  static_cast(h).data[i] = c;
}

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

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

Et l'interface n'a pas besoin d'exposer le fonctionnement interne de Bundle. Juste un Handle opaque.

Maintenant, cette technique peut être utile si d'autres parties du code doivent savoir que tous les Handles sont dans ce tampon spécifique, ou que leur durée de vie est suivie de manière spécifique. Il serait également possible de gérer cela avec des constructeurs privés et des fonctions d'usine amies.

16voto

Domso Points 745

Un scénario pourrait être la prévention de la mauvaise désallocation:

#include 

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 d'allocation/désallocation spécial (par exemple, une usine)

un exemple plus de style '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_container;
};

9voto

Matthieu M. Points 101624

Pourquoi marquer un destructeur comme delete ?

Pour empêcher l'appel du destructeur, 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 devrait être conservée en mémoire ; par exemple, une instance singleton de journalisation
  3. Une instance de cette classe ne peut être créée et libérée que par un mécanisme spécifique ; cela pourrait 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'encapsuler dans un unique_ptr pour automatiser la libération, mais que se passe-t-il si vous écrivez accidentellement : unique_ptr ? C'est un désastre en cours d'exécution !

Donc, au lieu de cela, vous pouvez ajuster la définition de la classe :

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

et alors le compilateur rejettera unique_ptr vous obligeant à utiliser correctement unique_ptr à 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 les supprimer et de permettre au système d'exploitation de nettoyer à la fin du programme.

Alternativement (et encore plus bizarre), vous pourriez allouer un tampon et y créer un objet, puis supprimer le tampon pour récupérer l'espace mais sans jamais essayer d'appeler le destructeur.

#include 

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("ne pas supprimer ceci...");//Constructs an object of type S in the buffer.
    //Code that uses s...
    std::cout<getx()<

``

Aucune de ces méthodes ne semble être une bonne idée, sauf dans des circonstances spécialisées. 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 ferait quelque chose de non trivial, il est très probable que vous compromettrez la validité de votre programme en ne parvenant pas à exécuter sa sémantique.

Laisser un programme quitter main() et permettre à l'environnement de 'nettoyer' est une technique valide mais à éviter autant que possible sauf si les contraintes le rendent strictement nécessaire. Au mieux, c'est un excellent moyen de masquer de véritables fuites de mémoire !

Je soupçonne que cette fonctionnalité est présente pour des raisons de complétude avec la possibilité de delete d'autres membres générés automatiquement.

J'adorerais voir une véritable utilisation pratique de cette capacité.

Il y a la notion d'une classe statique (sans constructeurs) et logiquement ne nécessitant aucun destructeur. Mais de telles classes sont plus correctement implémentées en tant que namespace et n'ont pas (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 la supprimer est le moyen le plus sûr d'implémenter un Singleton en C++, car cela évite tout problème lié à l'ordre de destruction. Un exemple typique de ce problème serait un Singleton de "Journalisation" qui est accédé dans le destructeur d'une autre classe Singleton. Alexandrescu a consacré une section entière dans son livre classique "Modern C++ Design" sur les moyens de faire face aux problèmes d'ordre de destruction dans les implémentations Singleton.

Un destructeur supprimé est utile afin que même la classe Singleton elle-même ne puisse accidentellement supprimer l'instance. Cela empêche également une utilisation abusive comme delete &SingletonClass::Instance() (si Instance() retourne une référence, comme elle devrait; il n'y a aucune raison pour qu'elle retourne un pointeur).

À la fin de la journée, rien de tout cela n'est vraiment remarquable, cependant. Et bien sûr, vous ne devriez pas utiliser de 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