Quelle est la différence conceptuelle entre finalement et un destructeur?

12

Tout d'abord, je sais très bien pourquoi il n'y a pas de construction «enfin» en C ++? mais une discussion de plus en plus longue sur une autre question semble justifier une question distincte.

Mis à part le problème qu'en finallyC # et Java ne peuvent essentiellement exister qu'une seule fois (== 1) par portée et qu'une seule portée peut avoir plusieurs (== n) destructeurs C ++, je pense qu'ils sont essentiellement la même chose. (Avec quelques différences techniques.)

Cependant, un autre utilisateur a fait valoir :

... J'essayais de dire qu'un dtor est intrinsèquement un outil pour (Release sematics) et finalement est intrinsèquement un outil pour (Commit semantics). Si vous ne voyez pas pourquoi: réfléchissez à la raison pour laquelle il est légitime de lancer des exceptions les unes sur les autres dans des blocs enfin, et pourquoi il n'en va pas de même pour les destructeurs. (Dans un certain sens, c'est une chose entre données et contrôle. Les destructeurs sont pour libérer des données, enfin pour libérer le contrôle. Ils sont différents; il est malheureux que C ++ les lie ensemble.)

Quelqu'un peut-il clarifier cela?

Martin Ba
la source

Réponses:

6
  • Transaction ( try)
  • Sortie / réponse d'erreur ( catch)
  • Erreur externe ( throw)
  • Erreur du programmeur ( assert)
  • Rollback (la chose la plus proche pourrait être les gardes de portée dans les langues qui les prennent en charge nativement)
  • Libération de ressources (destructeurs)
  • Flux de contrôle indépendant des transactions ( finally)

Impossible de trouver une meilleure description finallyque le flux de contrôle indépendant des transactions. Il ne correspond pas nécessairement directement à un concept de haut niveau dans le contexte d'un état d'esprit de récupération de transaction et d'erreur, en particulier dans un langage théorique qui a à la fois des destructeurs et finally.

Ce qui me manque le plus intrinsèquement, c'est une fonctionnalité de langage qui représente directement le concept de réduction des effets secondaires externes. Les gardes de portée dans des langues comme D sont la chose la plus proche à laquelle je peux penser qui est proche de représenter ce concept. Du point de vue du flux de contrôle, une restauration dans la portée d'une fonction particulière devrait distinguer un chemin exceptionnel d'un chemin normal, tout en automatisant simultanément la restauration implicitement de tout effet secondaire causé par la fonction en cas d'échec de la transaction, mais pas lorsque la transaction réussit. . C'est assez facile à faire avec les destructeurs si, par exemple, nous définissons un booléen sur une valeur comme succeededtrue à la fin de notre bloc try pour empêcher la logique de restauration dans un destructeur. Mais c'est une façon plutôt détournée de le faire.

Bien que cela puisse sembler ne pas économiser autant, l'inversion des effets secondaires est l'une des choses les plus difficiles à corriger (ex: ce qui rend si difficile l'écriture d'un conteneur générique sans exception).


la source
4

D'une certaine manière, ils le sont - de la même manière qu'une Ferrari et un transit peuvent tous deux être utilisés pour étouffer les magasins pour une pinte de lait, même s'ils sont conçus pour des usages différents.

Vous pouvez placer une construction try / finally dans chaque étendue et nettoyer toutes les variables définies dans l'étendue dans le bloc finally pour émuler un destructeur C ++. C'est, conceptuellement, ce que fait C ++ - le compilateur appelle automatiquement le destructeur lorsqu'une variable sort de la portée (c'est-à-dire à la fin du bloc de portée). Vous devez organiser votre essai / enfin, donc l'essai est la toute première chose et enfin la toute dernière chose dans chaque portée. Vous devez également définir une norme pour chaque objet afin d'avoir une méthode nommée spécifiquement qu'il utilise pour nettoyer son état que vous appelleriez dans le bloc finally, bien que je suppose que vous pouvez laisser la gestion de mémoire normale fournie par votre langage. nettoyez l'objet maintenant vidé quand il le souhaite.

Cependant, ce ne serait pas joli de le faire, et même si .NET a introduit IDispose en tant que destructeur géré manuellement, et en utilisant des blocs pour tenter de faciliter légèrement la gestion manuelle, ce n'est toujours pas quelque chose que vous voudriez faire dans la pratique .

gbjbaanb
la source
4

De mon point de vue, la principale différence est qu'un destructeur en c ++ est un mécanisme implicite (automatiquement appelé) pour libérer les ressources allouées tandis que la tentative ... enfin peut être utilisée comme mécanisme explicite pour le faire.

Dans les programmes c ++, le programmeur est responsable de la libération des ressources allouées. Ceci est généralement implémenté dans le destructeur d'une classe et fait immédiatement lorsqu'une variable sort de la portée ou lorsque la suppression est appelée.

Quand en c ++ une variable locale d'une classe est créée sans utiliser newles ressources de ces instances sont libérées implicitement par le destructeur quand il y a une exception.

// c++
void test() {
    MyClass myClass(someParameter);
    // if there is an exception the destructor of MyClass is called automatically
    // this does not work with
    // MyClass* pMyClass = new MyClass(someParameter);

} // on test() exit the destructor of myClass is implicitly called

En java, c # et autres systèmes avec une gestion automatique de la mémoire, le garbage collector des systèmes décide quand une instance de classe est détruite.

// c#
void test() {
    MyClass myClass = new MyClass(someParameter);
    // if there is an exception myClass is NOT destroyed so there may be memory/resource leakes

    myClass.destroy(); // this is never called
}

Il n'y a pas de mécanisme implicite pour cela, vous devez donc programmer cela explicitement en utilisant try enfin

// c#
void test() {
    MyClass myClass = null;

    try {
        myClass = new MyClass(someParameter);
        ...
    } finally {
        // explicit memory management
        // even if there is an exception myClass resources are freed
        myClass.destroy();
    }

    myClass.destroy(); // this is never called
}
k3b
la source
En C ++, pourquoi le destructeur est-il appelé automatiquement uniquement avec un objet pile et non avec un objet tas en cas d'exception?
Giorgio
@Giorgio Parce que les ressources de tas vivent dans un espace mémoire qui n'est pas directement lié à la pile d'appels. Par exemple, imaginez une application multithread avec 2 threads Aet B. Si un thread est lancé, l'annulation de la A'stransaction ne doit pas détruire les ressources allouées dans B, par exemple - les états du thread sont indépendants les uns des autres et la mémoire persistante vivant sur le tas est indépendante des deux. Cependant, généralement en C ++, la mémoire de tas est toujours liée aux objets de la pile.
@Giorgio Par exemple, un std::vectorobjet peut vivre sur la pile mais pointer vers la mémoire sur le tas - à la fois l'objet vectoriel (sur la pile) et son contenu (sur le tas) seraient désalloués lors d'un déroulement de pile dans ce cas, car la destruction du vecteur sur la pile invoquerait un destructeur qui libère la mémoire associée sur le tas (et détruirait également ces éléments de tas). En règle générale, pour des raisons de sécurité d'exception, la plupart des objets C ++ vivent sur la pile, même s'ils ne gèrent que la mémoire pointant sur le tas, automatisant le processus de libération du tas et de la mémoire de la pile lors du déroulement de la pile.
4

Heureux que vous ayez posté cette question. :)

J'essayais de dire que les destructeurs et finallysont conceptuellement différents:

  • Les destructeurs servent à libérer des ressources ( données )
  • finallyest pour retourner à l'appelant ( contrôle )

Considérons, disons, ce pseudo-code hypothétique:

try {
    bar();
} finally {
    logfile.print("bar has exited...");
}

finallyici résout entièrement un problème de contrôle et non un problème de gestion des ressources.
Cela n'aurait aucun sens de le faire dans un destructeur pour diverses raisons:

  • Aucune chose est « acquis » ou « créé »
  • Le fait de ne pas imprimer dans le fichier journal n'entraînera pas de fuites de ressources, de corruption de données, etc. (en supposant que le fichier journal ici n'est pas réinjecté ailleurs dans le programme)
  • Il est légitime logfile.printd'échouer, alors que la destruction (conceptuellement) ne peut pas échouer

Voici un autre exemple, cette fois comme en Javascript:

var mo_document = document, mo;
function observe(mutations) {
    mo.disconnect();  // stop observing changes to prevent re-entrance
    try {
        /* modify stuff */
    } finally {
        mo.observe(mo_document);  // continue observing (conceptually, this can fail)
    }
}
mo = new MutationObserver(observe);
return observe();

Dans l'exemple ci-dessus, encore une fois, il n'y a aucune ressource à libérer.
En fait, le finallybloc acquiert des ressources en interne pour atteindre son objectif, ce qui pourrait potentiellement échouer. Par conséquent, il n'est pas logique d'utiliser un destructeur (si Javascript en avait un).

D'un autre côté, dans cet exemple:

b = get_data();
try {
    a.write(b);
} finally {
    free(b);
}

finallyest la destruction d' une ressource, b. C'est un problème de données. Le problème n'est pas de retourner proprement le contrôle à l'appelant, mais plutôt d'éviter les fuites de ressources.
L'échec n'est pas une option et ne devrait (conceptuellement) jamais se produire.
Chaque version de best nécessairement associée à une acquisition, et il est logique d'utiliser RAII.

En d'autres termes, simplement parce que vous pouvez utiliser soit pour simuler soit cela ne signifie pas que les deux sont un seul et même problème ou que les deux sont des solutions appropriées pour les deux problèmes.

user541686
la source
Merci. Je ne suis pas d'accord, mais bon :-) Je pense que je pourrai ajouter une réponse d'opinion approfondie dans les prochains jours ...
Martin Ba
2
Comment le fait qui finallyest principalement utilisé pour libérer des ressources (autres que la mémoire) y tient -il compte?
Bart van Ingen Schenau,
1
@BartvanIngenSchenau: Je n'ai jamais prétendu qu'un langage existant a une philosophie ou une implémentation qui correspond à ce que j'ai décrit. Les gens n'ont pas encore fini d'inventer tout ce qui pourrait éventuellement exister. J'ai seulement soutenu qu'il serait utile de séparer les deux notions car ce sont des idées différentes et ont des cas d'utilisation différents. Pour satisfaire votre curiosité, je crois que D a les deux. Il existe probablement aussi d'autres langues. Je ne le considère pas pertinent cependant, et je me fiche de savoir pourquoi, par exemple, Java était en faveur de finally.
user541686
1
Un exemple pratique que j'ai rencontré en JavaScript sont des fonctions qui changent temporairement le pointeur de la souris en un sablier pendant une opération longue (ce qui pourrait lever une exception), puis le ramènent à la normale dans la finallyclause. La vision du monde C ++ introduirait une classe qui gère cette «ressource» d'une affectation à une variable pseudo-globale. Quel sens conceptuel cela fait-il? Mais les destructeurs sont le marteau de C ++ pour l'exécution de code de fin de bloc requise.
dan04
1
@ dan04: Merci beaucoup, c'est l'exemple parfait pour cela. Je pourrais jurer que je suis tombé sur tant de situations où RAII n'avait pas de sens mais j'ai eu tellement de mal à y penser.
user541686
1

La réponse de k3b l'exprime vraiment bien:

un destructeur en c ++ est un mécanisme implicite (invoqué automatiquement) pour libérer les ressources allouées tandis que try ... enfin peut être utilisé comme mécanisme explicite pour le faire.

Quant aux «ressources», j'aime me référer à Jon Kalb: RAII devrait signifier que l'acquisition de responsabilité est l'initialisation .

Quoi qu'il en soit, comme pour implicite vs explicite, cela semble vraiment être le cas:

  • Un d'tor est un outil pour définir quelles opérations doivent se produire - implicitement - à la fin de la durée de vie d'un objet (ce qui coïncide souvent avec la fin de la portée)
  • Un bloc finalement est un outil pour définir - explicitement - quelles opérations doivent se produire en fin de champ.
  • De plus, techniquement, vous êtes toujours autorisé à lancer enfin, mais voir ci-dessous.

Je pense que c'est tout pour la partie conceptuelle, ...


... maintenant il y a à mon humble avis des détails intéressants:

Je ne pense pas non plus que le c'tor / d'tor ait besoin conceptuellement d '"acquérir" ou de "créer" quoi que ce soit, à part la responsabilité d'exécuter du code dans le destructeur. C'est ce qui fait finalement aussi: exécuter du code.

Et bien que le code dans un bloc finalement puisse certainement lever une exception, ce n'est pas assez de distinction pour moi pour dire qu'ils sont conceptuellement différents au-dessus de l'explicite et de l'implicite.

(De plus, je ne suis pas du tout convaincu que le "bon" code devrait finalement disparaître - c'est peut-être une autre question en soi.)

Martin Ba
la source
Que pensez-vous de mon exemple Javascript?
user541686
Concernant vos autres arguments: "Voudrions-nous vraiment enregistrer la même chose sans égard?" Oui, c'est juste un exemple et vous manquez un peu le point, et oui, personne n'a jamais interdit d'enregistrer des détails plus spécifiques pour chaque cas. Le fait est que vous ne pouvez certainement pas prétendre qu'il n'y a jamais de situation dans laquelle vous voudriez enregistrer quelque chose qui est commun aux deux. Certaines entrées de journal sont génériques, d'autres spécifiques; vous voulez les deux. Et encore une fois, vous manquez complètement le point en vous concentrant sur la journalisation. Il est difficile de motiver des exemples de 10 lignes; s'il vous plaît essayez de ne pas manquer le point.
user541686
Vous n'avez jamais abordé ces ...
user541686
@Mehrdad - Je n'ai pas abordé votre exemple javascript car il me faudrait une autre page pour discuter de ce que j'en pense. (J'ai essayé, mais il m'a fallu si longtemps, de formuler quelque chose de cohérent que je l'ai sauté :-)
Martin Ba
@Mehrdad - comme pour vos autres points - il semble que nous devons accepter d'être en désaccord. Je vois où vous visez la différence, mais je ne suis tout simplement pas convaincu que ce soit quelque chose de conceptuellement différent: principalement parce que je suis principalement dans le camp qui pense que jeter enfin est une très mauvaise idée ( note : je pensez dans votre observerexemple que lancer là-bas serait une très mauvaise idée.) N'hésitez pas à ouvrir un chat, si vous voulez en discuter plus avant. C'était certainement amusant de réfléchir à vos arguments. À votre santé.
Martin Ba