web-dev-qa-db-fra.com

L'appel du destructeur manuellement est-il toujours un signe de mauvaise conception?

Je pensais: ils disent que si vous appelez destructeur manuellement, vous faites quelque chose de mal. Mais est-ce toujours le cas? Y a-t-il des contre-exemples? Des situations où il est nécessaire de l'appeler manuellement ou où il est difficile/impossible/peu pratique de l'éviter?

71
Violet Giraffe

L'appel manuel du destructeur est requis si l'objet a été construit avec une forme surchargée de operator new(), sauf en cas d'utilisation de la surcharge "std::nothrow":

T* t0 = new(std::nothrow) T();
delete t0; // OK: std::nothrow overload

void* buffer = malloc(sizeof(T));
T* t1 = new(buffer) T();
t1->~T(); // required: delete t1 would be wrong
free(buffer);

En dehors de la gestion de la mémoire à un niveau assez bas, comme précédemment, cependant, est un signe de mauvaise conception. En fait, il s’agit probablement non seulement d’une mauvaise conception, mais aussi d’une fausse erreur (oui, l’utilisation d’un destructeur explicite suivi d’un appel du constructeur de copie dans l’opérateur d’assignation est est une mauvaise conception et est susceptible d’être erronée).

Avec C++ 2011, il existe une autre raison d'utiliser des appels de destructeurs explicites: lorsque vous utilisez des unions généralisées, il est nécessaire de détruire explicitement l'objet actuel et de créer un nouvel objet à l'aide de placement new lors du changement de type de l'objet représenté. De plus, lorsque l'union est détruite, il est nécessaire d'appeler explicitement le destructeur de l'objet actuel s'il doit être détruit.

80
Dietmar Kühl

Toutes les réponses décrivent des cas spécifiques, mais il existe une réponse générale:

Vous appelez explicitement le dtor chaque fois que vous devez simplement détruire le objet (au sens de C++) sans libérer le mémoire l 'objet réside dans.

Cela se produit généralement dans toutes les situations où l'allocation/désallocation de mémoire est gérée indépendamment de la construction/destruction des objets. Dans ces cas, la construction se fait via placement new sur un bloc de mémoire existant, et la destruction se produit par appel explicite de dtor.

Voici l'exemple brut:

{
  char buffer[sizeof(MyClass)];

  {
     MyClass* p = new(buffer)MyClass;
     p->dosomething();
     p->~MyClass();
  }
  {
     MyClass* p = new(buffer)MyClass;
     p->dosomething();
     p->~MyClass();
  }

}

Un autre exemple notable est le std::allocator par défaut utilisé par std::vector: les éléments sont construits dans vector pendant Push_back, mais la mémoire est allouée par morceaux, de sorte qu'elle préexiste à la construction. Et par conséquent, vector::erase doit détruire les éléments, mais pas nécessairement, il désallouera la mémoire (surtout si un nouveau Push_back doit arriver bientôt ...).

C'est une "mauvaise conception" au sens strict du terme OOP (vous devez gérer des objets, pas de la mémoire: le fait que les objets nécessitent de la mémoire est un "incident"), il s'agit d'une "bonne conception" dans Dans les cas où la mémoire n’est pas extraite du "magasin libre", le operator new par défaut achète.

C'est une mauvaise conception si cela se produit de manière aléatoire autour du code, mais une bonne conception s'il se produit localement dans des classes spécialement conçues à cet effet.

86
Emilio Garavaglia

Non, cela dépend de la situation. Parfois, il est légitime et conçu de manière {bonne}.

Pour comprendre pourquoi et quand appeler des destructeurs de manière explicite, examinons ce qui se passe avec "new" et "delete".

Pour créer un objet de manière dynamique, T* t = new T; sous le capot: 1. La taille de la mémoire (T) est allouée. 2. Le constructeur de T est appelé pour initialiser la mémoire allouée. L'opérateur new fait deux choses: l'allocation et l'initialisation.

Pour détruire l'objet delete t; sous le capot: 1. Le destructeur de T est appelé. 2. La mémoire allouée pour cet objet est libérée. l'opérateur delete fait également deux choses: destruction et désallocation. 

On écrit le constructeur pour faire l'initialisation, et le destructeur pour faire la destruction. Lorsque vous appelez explicitement le destructeur, seule la destruction est effectuée, mais pas la désallocation.

Par conséquent, une utilisation légitime du destructeur appelant explicitement pourrait être: "Je veux seulement détruire l'objet, mais je ne libère pas (ou ne peux pas) l'allocation de mémoire (pour l'instant)".

Un exemple courant consiste à pré-allouer de la mémoire pour un pool de certains objets qui doivent sinon être alloués de manière dynamique. 

Lors de la création d'un nouvel objet, vous obtenez le bloc de mémoire du pool préalloué et effectuez un "placement nouveau". Une fois que vous avez terminé avec l'objet, vous pouvez appeler explicitement le destructeur pour terminer le travail de nettoyage, le cas échéant. Mais vous ne désaffecterez pas réellement la mémoire, comme l’aurait fait l’opérateur delete. Au lieu de cela, vous renvoyez le bloc dans le pool pour le réutiliser. 

8
user1252446

Comme indiqué dans la FAQ, vous devez appeler le destructeur explicitement lorsque vous utilisez placement new .

C'est à peu près la seule fois où vous appelez explicitement un destructeur.

Je conviens cependant que cela est rarement nécessaire.

7
Luchian Grigore

Non, vous ne devriez pas l'appeler explicitement car il serait appelé deux fois. Une fois pour l'appel manuel et une autre fois lorsque la portée dans laquelle l'objet est déclaré se termine.

Par exemple.

{
  Class c;
  c.~Class();
}

Si vous devez réellement effectuer les mêmes opérations, vous devez utiliser une méthode distincte.

Il existe une situation spécifique dans laquelle vous pouvez appeler un destructeur sur un objet alloué de manière dynamique avec un emplacement new mais qui ne sonne pas comme ce dont vous aurez besoin.

6
Jack

À chaque fois que vous avez besoin de séparer l’allocation de l’initialisation, vous aurez besoin d’un nouvel appel et d’un appel explicite du destructeur Manuellement. Aujourd'hui, cela est rarement nécessaire, car nous avons les conteneurs standard , Mais si vous devez implémenter un nouveau type de conteneur , Vous en aurez besoin. 

4
James Kanze

Il y a des cas où ils sont nécessaires:

Dans le code sur lequel je travaille, j’utilise un appel de destructeur explicite dans les allocateurs, j’ai mis en œuvre un simple allocateur qui utilise un nouvel emplacement pour renvoyer des blocs de mémoire à des conteneurs stl. En détruire j'ai:

  void destroy (pointer p) {
    // destroy objects by calling their destructor
    p->~T();
  }

en construction:

  void construct (pointer p, const T& value) {
    // initialize memory with placement new
    #undef new
    ::new((PVOID)p) T(value);
  }

des allocations sont également effectuées dans allocate () et des allocations de mémoire dans deallocate (), en utilisant des mécanismes alloc et dealloc spécifiques à la plate-forme. Cet allocateur était utilisé pour contourner doug lea malloc et utiliser directement, par exemple, LocalAlloc sur Windows.

2
marcinj

Je n'ai jamais rencontré une situation où il est nécessaire d'appeler un destructeur manuellement. Je crois me rappeler que même Stroustrup prétend que c'est une mauvaise pratique.

1
Lieuwe

Et ça? 
Destructor n'est pas appelé si une exception est levée du constructeur. Je dois donc l'appeler manuellement pour détruire les handles créés dans le constructeur avant l'exception.

class MyClass {
  HANDLE h1,h2;
  public:
  MyClass() {
    // handles have to be created first
    h1=SomeAPIToCreateA();
    h2=SomeAPIToCreateB();
    ...
    try {
      if(error) {
        throw MyException();
      }
    }
    catch(...) {
      this->~MyClass();
      throw;
    }
  }
  ~MyClass() {
    SomeAPIToDestroyA(h1);
    SomeAPIToDestroyB(h2);
  }
};
1
CITBL

J'ai trouvé 3 occasions où je devais le faire:

  • attribution/désallocation d'objets en mémoire créés par memory-mapped-io ou mémoire partagée
  • lors de l'implémentation d'une interface C donnée utilisant C++ (oui, malheureusement, cela se produit encore aujourd'hui (car je n'ai pas assez de poids pour la changer))
  • lors de l'implémentation de classes allocateur
0
user4590120