web-dev-qa-db-fra.com

Est-ce que delete est autorisé à modifier son paramètre?

Dans une réponse, https://stackoverflow.com/a/704568/8157187 , il y a une citation de Stroustrup:

C++ autorise explicitement une implémentation de delete à supprimer un opérande lvalue, et j'avais espéré que les implémentations le feraient, mais cette idée ne semble pas être devenue populaire auprès des implémenteurs.

Cependant, je n'ai pas réussi à trouver cette déclaration explicite dans la norme. Il existe une partie du projet de norme actuel (N4659), que l'on peut interpréter de cette façon:

6.7:

Lorsque la fin de la durée d'une région de stockage est atteinte, les valeurs de tous les pointeurs représentant l'adresse de toute partie de cette région de stockage deviennent des valeurs de pointeur non valides (6.9.2). L'indirection via une valeur de pointeur non valide et le passage d'une valeur de pointeur non valide à une fonction de désallocation ont un comportement indéfini. Toute autre utilisation d'une valeur de pointeur invalide a un comportement défini par l'implémentation.

Note de bas de page: Certaines implémentations peuvent définir que la copie d'une valeur de pointeur non valide provoque une erreur d'exécution générée par le système.

Ainsi, après un delete ptr;, la valeur de ptr devient une valeur de pointeur non valide et son utilisation a un comportement défini par la mise en oeuvre. Cependant, cela ne dit pas que la valeur de ptr est autorisée à changer.

Ce pourrait être une question philosophique, comment peut-on décider qu'une valeur a changé, si on ne peut pas utiliser sa valeur?

6.9:

Pour tout objet (autre qu'un sous-objet de classe de base) de type T trivialement copiable, que l'objet contienne ou non une valeur valide de type T, les octets sous-jacents (4.4) constituant l'objet peuvent être copiés dans un tableau de caractères, unsigned char, ou std :: byte (21.2.1) .43 Si le contenu de ce tableau est recopié dans l'objet, celui-ci conservera ensuite sa valeur d'origine.

Donc, il semble qu'il soit valide de memcpy une valeur de pointeur invalide dans un tableau de caractères (en fonction de la déclaration "plus forte", 6.7 ou 6.9. Pour moi, 6.9 semble plus fort).

De cette façon, je peux détecter que la valeur du pointeur a été modifiée par delete: memcpy la valeur du pointeur avant et après le tableau delete to char, puis les comparer.

Donc, si j'ai bien compris, 6.7 n'accorde pas à delete le droit de modifier ses paramètres.

Est-ce que delete est autorisé à modifier son paramètre?

Découvrez les commentaires ici: https://stackoverflow.com/a/45142972/8157187


Voici un code du monde réel improbable, mais toujours possible, où cela compte:

SomeObject *o = ...; // We have a SomeObject
// This SomeObject is registered into someHashtable, with its memory address
// The hashtable interface is C-like, it handles opaque keys (variable length unsigned char arrays)

delete o;

unsigned char key[sizeof(o)];
memcpy(key, &o, sizeof(o)); // Is this line OK? Is its behavior implementation defined?
someHashtable.remove(key, sizeof(key)); // Remove o from the hashtable

Bien sûr, cet extrait peut être réorganisé, ce qui en fait un code sûrement valide. Mais la question est: est-ce un code valide?


Voici un fil de pensée connexe: supposons qu’une implémentation définisse ce que la note de bas de page décrit:

la copie d'une valeur de pointeur non valide provoque une erreur d'exécution générée par le système

6.9 garantit que je peux memcpy() n’importe quelle valeur. Même invalide. Donc, dans cette implémentation théorique, lorsque je memcpy() la valeur du pointeur invalide (qui devrait réussir, 6.9 le garantit), dans un sens, je n'utilise pas la valeur du pointeur invalide, mais uniquement ses octets sous-jacents (car cela générerait une erreur d'exécution , et 6.9 ne le permettent pas), donc 6.7 ne s'applique pas.

36
geza

Avant la suppression, la valeur de ptr était valide. Après la suppression, la valeur n'était pas valide. Par conséquent, la valeur a changé. Les valeurs valides et les valeurs non valables s'excluent mutuellement - une valeur ne peut pas être simultanément valide et non valide.

Votre question a une idée fausse de base; vous confondez ces deux concepts différents:

  • La valeur d'une variable
  • La représentation d'une variable en mémoire.

Il n'y a pas de correspondance un à un entre ces deux choses. La même valeur peut avoir plusieurs représentations et la même représentation peut correspondre à des valeurs différentes. 


Je pense que l’essentiel de votre question est:delete ptr; peut-il changer la représentation de ptr? . Pour lequel la réponse est "Oui". Vous pouvez mémoriser le pointeur supprimé dans un tableau de caractères, inspecter les octets et les trouver comme des octets de valeur zéro (ou autre chose). Cela est couvert dans la norme par C++ 14 [basic.stc.dynamic.deallocation]/4 (ou C++ 17 [basic.stc]/4):

Toute autre utilisation d'une valeur de pointeur non valide a un comportement défini par l'implémentation. 

Elle est définie par l'implémentation et l'implémentation pourrait définir que l'inspection des octets donne des octets de valeur zéro.


Votre extrait de code repose sur un comportement défini par l'implémentation. "Code valide" n'est pas la terminologie utilisée par la norme, mais il se peut que le code ne supprime pas l'élément souhaité de la table de hachage.

Comme Stroustrup l'a mentionné, il s'agit d'une décision de conception intentionnelle. Un exemple d'utilisation serait un compilateur en mode débogage définissant des pointeurs supprimés vers une représentation particulière, de sorte qu'il puisse générer une erreur d'exécution si un pointeur supprimé est utilisé par la suite. Voici un exemple de ce principe en action pour les pointeurs non initialisés.

Note historique: En C++ 11, il s'agissait de undefined , plutôt que défini par l'implémentation. Ainsi, le comportement d'utilisation d'un pointeur supprimé était identique à celui d'un pointeur non initialisé. Dans le langage C, libérer de la mémoire consiste à placer tous les pointeurs sur cette mémoire dans le même état qu’un pointeur non initialisé.

12
M.M

Le contexte dans lequel vous avez trouvé l’instruction de Stroustrup est disponible sous Stroustrup, delete zero

Stroustrup vous laisse envisager

delete p;
// ...
delete p;

Après la première suppression, le pointeur p était invalide. La deuxième suppression est incorrecte, mais cela n'aurait aucun effet si p était défini sur 0 après la première suppression.

L’idée de Stroustrups était de le compiler comme suit: 

delete p; p = 0;
// ...
delete p;

supprimer lui-même n'est pas en mesure de mettre à zéro le pointeur puisqu'il a passé void * mais pas void *&

Cependant, je trouve que le point zéro p n'aide pas beaucoup car il peut exister d'autres copies de ce pointeur qui pourraient également être supprimées accidentellement.

Spec 6.7

Indique que tout pointeur portant l'adresse du pointeur p sera invalide (d'une manière définie par l'implémentation) après une suppression p. Cela ne dit rien sur la modification de l'adresse du pointeur, ni autorisée ni interdite.

Spec 6.9

La condition préalable de 6.9 est un objet (valide ou non). Cette spécification ne s'applique pas ici car p (l'adresse) n'est plus valide après la suppression et ne pointe donc PAS vers un objet. Donc, il n'y a pas de contradiction, et toute discussion pour savoir si 6.7 ou 6.9 est plus fort est invalide.

La spécification nécessite également de copier les octets dans l'emplacement d'origine de l'objet, ce que votre code ne peut pas et ne pourrait pas, car l'objet original a été supprimé.


Cependant, je ne vois aucune raison de compresser une adresse de pointeur dans un tableau de caractères et de la transmettre. Et les pointeurs ont toujours la même taille dans une certaine implémentation. Votre code est juste une version encombrante de:

SomeObject *o = ...;

void *orgO = o;
delete o;

someHashtable.remove(orgO);
// someHashtable.remove(o); o might be set to 0

Cependant, ce code semble toujours étrange. Pour obtenir un objet de la table de hachage, vous avez besoin du pointeur sur cet objet. Pourquoi ne pas utiliser directement le pointeur directement ??

Une table de hachage devrait aider à trouver des objets à partir de certaines valeurs invariantes des objets. Ce n'est pas votre application de table de hachage

Avez-vous voulu avoir une liste de toutes les instances valides de SomeObject?

Votre code est invalide car , selon Stroustrup, le compilateur est autorisé à définir p sur zéro. Si cela se produit, votre code va planter 

2
stefan bachert

delete est défini dans [expr.delete] pour appeler une fonction de désallocation, et les fonctions de désallocation sont définies dans [basic.stc.dynamic.deallocation] comme suit:

Chaque fonction de désallocation doit renvoyer void et son premier paramètre doit être void*.

Étant donné que toutes les fonctions de désallocation obtiennent void*, et non un void*&, il n’existe aucun mécanisme leur permettant de modifier leurs paramètres. 

2
Barry

L'indirection via une valeur de pointeur non valide et le passage d'une valeur de pointeur non valide à une fonction de désallocation ont un comportement indéfini. Toute autre utilisation d'une valeur de pointeur non valide a un comportement défini par l'implémentation.

Ainsi, après un delete ptr;, la valeur de ptr devient une valeur de pointeur non valide et son utilisation a un comportement défini par la mise en oeuvre.

La norme dit que la valeur de pointeur transmise "devient invalide", c'est-à-dire que son statut a changé de sorte que certains appels deviennent indéfinis et que l'implémentation peut le traiter différemment.

Le langage n'est pas très clair, mais voici le contexte:

6.7 Durée de stockage
4 Lorsque la fin de la durée d'une région de stockage est atteinte, les valeurs de tous les pointeurs représentant l'adresse de toute partie de cette région de stockage deviennent des valeurs de pointeur non valides (6.9.2). L'indirection via une valeur de pointeur non valide et le passage d'une valeur de pointeur non valide à une fonction de désallocation ont un comportement indéfini. Toute autre utilisation d'une valeur de pointeur non valide a un comportement défini par l'implémentation.

6.9.2 Types de composés
Chaque valeur de type pointeur est l’une des suivantes:
(3.1) - un pointeur sur un objet ou une fonction (le pointeur est censé pointer sur l'objet ou la fonction), ou
(3.2) - un pointeur situé au-delà de la fin d'un objet (8.7), ou
(3.3) - la valeur du pointeur nul (7.11) pour ce type, ou
(3.4) - une valeur de pointeur non valide. 

Ce sont les valeurs de type pointeur qui sont ou ne sont pas invalides, et elles "deviennent" ainsi en fonction de la progression de l’exécution d’un programme sur la machine abstraite C++.

La norme ne parle pas de modification de la valeur détenue par la variable/l'objet adressé par une valeur ou de la modification de l'association d'un symbole à une valeur.

C++ autorise explicitement une implémentation de delete à supprimer un opérande Lvalue, et j'avais espéré que les implémentations le feraient, , Mais cette idée ne semble pas être devenue populaire auprès des implémenteurs.

Séparément de cela, Stroustrup dit que si une expression d'opérande était une lvalue modifiable, c'est-à-dire qu'une expression d'opérande était l'adresse d'une variable/d'un objet contenant une valeur de pointeur passée, après quoi l'état de cette valeur est " invalid ", alors la mise en oeuvre peut définir la valeur détenue par cette variable/cet objet à zéro.

Cependant, cela ne dit pas que la valeur de ptr est autorisée à changer.

Stroustrup se veut informel en parlant de ce qu'une implémentation peut faire. La norme définit comment une machine C++ abstraite peut/ne peut/se comporte. Stroustrup parle ici d’une implémentation hypothétique qui ressemble à cette machine. Une "valeur de ptr" est "autorisée à changer" car le comportement défini et indéfini ne vous permet pas de savoir quelle est cette valeur en tant que de désallocation, et le comportement défini par l'implémentation peut être quelconque, de sorte que la variable/objet détient une valeur différente.

Cela n'a pas de sens de parler de changement de valeur. Vous ne pouvez pas "mettre à zéro" une valeur; vous pouvez mettre à zéro une variable/objet, et c'est ce que nous voulons dire lorsque nous disons "mettre à zéro" une lvalue - mettre à zéro la variable/objet qu'elle référence/identifie. Même si vous étendez "zéro sur" pour inclure l'association d'une nouvelle valeur à un nom ou à un littéral, l'implémentation peut le faire, car vous ne pouvez pas "utiliser" la valeur au moment de l'exécution via le nom ou le littéral pour savoir si elle est toujours associée à la même valeur.

(Cependant, puisque tout ce que l'on peut faire avec une valeur, c'est "l'utiliser" dans un programme en transmettant une lvalue identifiant une variable/un objet le conservant à un opérateur ou en transmettant une référence ou une constante le désignant à un opérateur, et un opérateur peut agissez comme si un différent valeur était passé, je suppose que vous pouvez raisonnablement saisir de manière informelle que négligemment cela par "des valeurs qui changent de valeur" dans la mise en oeuvre.)

Si le contenu de ce tableau est recopié dans l'objet, celui-ci conservera ensuite sa valeur d'origine.

Mais copier c'est utiliser, donc copier est défini par l'implémentation une fois qu'il est "invalide". Donc, appeler un programme qui le copierait normalement est défini par l'implémentation. C’est ce qui ressort clairement de la note de bas de page qui donne l’exemple suivant: 

Certaines implémentations peuvent définir que la copie d'une valeur de pointeur non valide provoque une erreur d'exécution générée par le système.

Rien ne fait ce qu’il fait normalement en tant que comportement indéfini/défini par la mise en oeuvre. Nous utilisons le comportement normal pour déterminer une séquence de modifications sur une machine abstraite. Si un changement d'état défini par l'implémentation se produit, les choses agissent comme si l'implémentation les définissait pour agir, et non comme elles le font normalement. Malheureusement, le sens de "utilisation de" une valeur n'est pas précisé. Je ne sais pas pourquoi vous pensez que 6.9 "garantit" quelque chose de plus ou moins mémorable que partout ailleurs, ce qui après un état non défini/défini par la mise en oeuvre n’est plus rien.

2
philipxy

Pour qu'une fonction de suppression mette à jour le pointeur, elle doit connaître l'adresse de ce pointeur et également effectuer la mise à jour. Cela nécessiterait de la mémoire supplémentaire, quelques opérations supplémentaires et un support du compilateur. Cela semble plus ou moins trivial dans votre exemple. 

Imaginons maintenant une chaîne de fonctions qui se font passer le pointeur en arguments et que seul le dernier supprime réellement. Quels pointeurs mettre à jour dans un tel cas? Le dernier? Tout? Pour ces derniers, il faudrait créer une liste dynamique de pointeurs:

Objec *o = ...
handle(o);
void handle(Object *o){
   if (deleteIt) doDelete(0);
   else doSomethingElseAndThenPossiblyDeleteIt(o);
}
void doDelete(Object *o) {
    delete o;
}

Ainsi, philosophiquement, si la suppression était autorisée à modifier son paramètre, elle ouvrirait une boîte de conserve réduisant l'efficacité du programme. Donc, ce n'est pas permis et j'espère que ça ne le sera jamais. Le comportement indéfini est probablement la chose la plus naturelle dans ces cas. 

En ce qui concerne le contenu de la mémoire, j’ai malheureusement vu trop d’erreurs dans lesquelles une mémoire supprimée était écrasée après la suppression du pointeur. Et ... ça marche jusqu'à ce qu'un moment arrive. Étant donné que la mémoire est marquée comme libre, elle est réutilisée par d’autres objets, ce qui entraîne des conséquences inintéressantes et beaucoup de débogage. Donc, philosophiquement encore, c ++ n’est pas un langage facile à programmer. Il existe d'autres outils susceptibles de résoudre ces problèmes, sans aucune prise en charge linguistique. 

1
Serge