Considérez la classe suivante:
struct S { ~S() = delete; };
En bref et aux fins 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 le supprimer également.
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éfinissez une classe qui expose uniquement un tas de fonctions statiques et interdisez toute tentative de création d'une instance de cette classe.
Quelles sont les autres utilisations (le cas échéant) d'un destructeur supprimé?
Si vous avez un objet qui ne doit 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;
}
Nous avons ici des Handle
s 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 ci-dessus n'est un comportement non défini; ne pas détruire un Bundle
est acceptable, tout comme en créer un nouveau à sa place.
Et l'interface n'a pas à exposer le fonctionnement de Bundle
. Juste un Handle
opaque.
Maintenant, cette technique peut être utile si d'autres parties du code doivent savoir que tous les descripteurs se trouvent 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é par des constructeurs privés et des fonctions d'usine amie.
un scénario pourrait être la prévention d'une mauvaise répartition:
#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/désallocation 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;
};
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:
Pour illustrer ce dernier point, imaginez une interface C:
struct Handle { /**/ };
Handle* xyz_create();
void xyz_dispose(Handle*);
En C++, vous voudriez l'envelopper dans un unique_ptr
pour automatiser la publication, mais que se passe-t-il si vous écrivez accidentellement: unique_ptr<Handle>
? C'est une catastrophe au moment de l'exécution!
Donc, à la place, vous pouvez modifier la définition de classe:
struct Handle { /**/ ~Handle() = delete; };
puis le compilateur s'étouffera sur unique_ptr<Handle>
vous obligeant à utiliser correctement unique_ptr<Handle, xyz_dispose>
au lieu.
Il existe deux cas d'utilisation plausibles. Tout d'abord (comme le notent certains commentaires), il peut être acceptable d'allouer dynamiquement des objets, de ne pas les delete
et de permettre au système d'exploitation de 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 inviter à essayer 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;
}
Rien de tout cela ne semble être une bonne idée, sauf dans des circonstances particulières. Si le destructeur créé automatiquement ne fait rien (car le destructeur de tous les membres est trivial), alors le compilateur crée un destructeur sans effet.
Si le destructeur créé automatiquement ferait quelque chose de non trivial, vous compromettez très probablement la validité de votre programme en omettant d'exécuter sa sémantique.
Laisser un programme quitter main()
et permettre à l'environnement de "nettoyer" est une technique valide mais mieux vaut éviter, sauf si les contraintes 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 pour être complète avec la possibilité de delete
d'autres membres générés automatiquement.
J'aimerais voir une réelle utilisation pratique de cette capacité.
Il y a la notion d'une classe statique (sans constructeurs) et donc logiquement ne nécessitant aucun destructeur. Mais ces classes sont mieux implémentées car un namespace
n'a pas de (bonne) place dans le C++ moderne à moins qu'il ne soit modélisé.
Créer une instance d'un objet avec new
et ne jamais le 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 déjà consacré une section entière de son livre classique "Modern C++ Design" sur les moyens de faire face aux problèmes d'ordre de destruction dans les implémentations de Singleton.
Un destructeur supprimé est agréable à avoir afin que même la classe Singleton elle-même ne puisse pas supprimer accidentellement l'instance. Il empêche également une utilisation folle comme delete &SingletonClass::Instance()
(si Instance()
renvoie une référence, comme il se doit; il n'y a aucune raison pour qu'il retourne un pointeur).
En fin de compte, rien de tout cela n'est vraiment remarquable, cependant. Et bien sûr, vous ne devriez pas utiliser les singletons en premier lieu de toute façon.