Les utilisations de destructeur = supprimer;

Considérons la classe suivante:

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

Bref et dans le but de la question: Je ne peux pas créer des instances de S comme S s{}; car je ne pouvais pas 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 la supprimer aussi bien.
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();
}

C'est-à-dire définir une classe qui n'expose qu'un tas 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é?

44
demandé sur skypjack 2016-11-22 16:18:30

5 réponses

Si vous avez un objet qui ne devrait jamais, jamais être delete D ou stocké sur la pile (stockage automatique), ou stocké dans le cadre d'un autre objet, =delete empêchera 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 des Handleopaques 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'une matrice.

Je crois que rien ci-dessus n'est un comportement indéfini; ne pas détruire un Bundle est acceptable, tout comme en créer un nouveau à sa place.

Et le l'interface n'a pas à exposer comment fonctionne Bundle. Juste un opaque Handle.

Maintenant, cette technique peut être utile si d'autres parties du code ont besoin de 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. Peut-être que cela pourrait également être géré avec des constructeurs privés et des fonctions d'usine ami.

20
répondu Yakk - Adam Nevraumont 2016-11-23 00:02:27

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

Un exemple de style 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;
};
16
répondu Domso 2016-11-23 11:36:07

Pourquoi marquer un destructeur comme delete?

Pour empêcher le destructeur d'être invoqué, bien sûr ;)

Quels sont les cas d'utilisation?

Je peux voir , au moins 3 utilisations différentes:

  1. la classe ne doit jamais être instanciée; dans ce cas, je m'attendrais également à un constructeur par défaut supprimé.
  2. une instance de cette classe doit être divulguée; par exemple, une instance singleton de journalisation
  3. Une instance de cette classe ne peut être créé et éliminé 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 pouvez l'envelopper dans un unique_ptr pour automatiser la publication, mais que faire si vous avez accidentellement écrire: unique_ptr<Handle>? C'est une catastrophe!

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

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

Et puis le compilateur s'étouffe sur unique_ptr<Handle> vous forçant à utiliser correctement unique_ptr<Handle, xyz_dispose> à la place.

9
répondu Matthieu M. 2016-11-22 15:52:41

Il y a 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 delete et de permettre au système d'exploitation de les nettoyer à la fin du programme.

Alternativement (et encore plus bizarre) vous pouvez allouer un tampon et créer un objet dedans, puis supprimer le tampon pour récupérer l'endroit mais ne jamais Demander une tentative d'appeler le 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 de ceux-ci ne semble être une bonne idée sauf en spécialiste circonstance. Si le destructeur créé automatiquement ne ferait rien (parce que le destructeur de tous les membres est trivial) alors le compilateur créera un destructeur sans effet.

Si le destructeur créé automatiquement ferait quelque chose de non trivial, vous compromettrez très probablement la validité de votre programme en échouant à exécuter sa sémantique.

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

Je soupçonne que la fonctionnalité est présente pour être complète avec la possibilité de {[1] } autres membres générés automatiquement.

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

Il y a la notion d'une classe statique (sans constructeur) et donc logiquement ne nécessitant aucun destructeur. Mais de telles classes sont implémentées de manière plus appropriée en tant que namespace n'ont pas de (bonne) place dans le C++ moderne sauf si modèle.

7
répondu Persixty 2016-11-22 14:19:31

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 C++, car il é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 dans son livre classique "Modern C++ Design" sur les moyens de faire face aux problèmes d'ordre de destruction dans Singleton application.

Un destructeur supprimé est agréable à avoir pour que même la classe Singleton elle-même ne puisse pas supprimer accidentellement l'instance. Il empêche aussi fou d'utilisation comme delete &SingletonClass::Instance() (si Instance() renvoie une référence, comme il se doit; 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 Singletons en premier lieu de toute façon.

5
répondu Christian Hackl 2016-11-22 18:45:08