Est-il sûr de supprimer un pointeur vide?

96

Supposons que j'ai le code suivant:

void* my_alloc (size_t size)
{
   return new char [size];
}

void my_free (void* ptr)
{
   delete [] ptr;
}

Est-ce sûr? Ou doit-il ptrêtre casté char*avant la suppression?

An̲̳̳drew
la source
Pourquoi faites-vous vous-même la gestion de la mémoire? Quelle structure de données créez-vous? Le besoin de faire une gestion explicite de la mémoire est assez rare en C ++; vous devriez généralement utiliser des classes qui le gèrent pour vous à partir de la STL (ou de Boost à la rigueur).
Brian
6
Juste pour les gens qui lisent, j'utilise des variables void * comme paramètres pour mes threads dans win c ++ (voir _beginthreadex). Habituellement, ils pointent vers des classes.
ryansstack
3
Dans ce cas, il s'agit d'un wrapper à usage général pour new / delete, qui pourrait contenir des statistiques de suivi d'allocation ou un pool de mémoire optimisé. Dans d'autres cas, j'ai vu des pointeurs d'objet stockés de manière incorrecte en tant que variables membres void * et supprimés de manière incorrecte dans le destructeur sans renvoyer au type d'objet approprié. J'étais donc curieux de connaître la sécurité / les pièges.
An̲̳̳drew le
1
Pour un wrapper à usage général pour new / delete, vous pouvez surcharger les opérateurs new / delete. Selon l'environnement que vous utilisez, vous obtenez probablement des hooks dans la gestion de la mémoire pour suivre les allocations. Si vous vous retrouvez dans une situation où vous ne savez pas ce que vous supprimez, considérez cela comme un indice fort que votre conception n'est pas optimale et doit être refactorisée.
Hans
38
Je pense qu'il y a trop de questions sur la question au lieu d'y répondre. (Pas seulement ici, mais dans tous les SO)
Petruza

Réponses:

24

Cela dépend de «sûr». Cela fonctionnera généralement parce que les informations sont stockées avec le pointeur sur l'allocation elle-même, de sorte que le désallocateur puisse les renvoyer au bon endroit. En ce sens, il est "sûr" tant que votre allocateur utilise des balises de limite internes. (Beaucoup le font.)

Cependant, comme mentionné dans d'autres réponses, la suppression d'un pointeur vide n'appellera pas de destructeurs, ce qui peut être un problème. En ce sens, ce n'est pas «sûr».

Il n'y a aucune bonne raison de faire ce que vous faites comme vous le faites. Si vous souhaitez écrire vos propres fonctions de désallocation, vous pouvez utiliser des modèles de fonction pour générer des fonctions avec le type correct. Une bonne raison de le faire est de générer des allocateurs de pool, qui peuvent être extrêmement efficaces pour des types spécifiques.

Comme mentionné dans d'autres réponses, il s'agit d' un comportement non défini en C ++. En général, il est bon d'éviter les comportements indéfinis, bien que le sujet lui-même soit complexe et rempli d'opinions contradictoires.

Christophe
la source
9
Comment est-ce une réponse acceptée? Cela n'a aucun sens de «supprimer un pointeur vide» - la sécurité est un point discutable.
Kerrek SB
6
"Il n'y a aucune bonne raison de faire ce que vous faites comme vous le faites." C'est votre opinion, pas un fait.
rxantos
8
@rxantos Fournissez un contre-exemple où faire ce que l'auteur de la question veut faire est une bonne idée en C ++.
Christopher
Je pense que cette réponse est en fait plutôt raisonnable, mais je pense aussi que toute réponse à cette question doit au moins mentionner qu'il s'agit d'un comportement indéfini.
Kyle Strand
@Christopher Essayez d'écrire un système de ramasse-miettes unique qui n'est pas spécifique au type mais fonctionne simplement. Le fait quesizeof(T*) == sizeof(U*) for all T,Usuggère qu'il devrait être possible d'avoir une void *implémentation de garbage collector non basée sur un modèle . Mais alors, lorsque le gc doit effectivement supprimer / libérer un pointeur, exactement cette question se pose. Pour le faire fonctionner, vous avez soit besoin de wrappers de destructeurs de fonctions lambda (urgh), soit d'une sorte de "type en tant que données" dynamique qui permet des allers-retours entre un type et quelque chose de stockable.
BitTickler
147

La suppression via un pointeur void n'est pas définie par le standard C ++ - voir section 5.3.5 / 3:

Dans la première alternative (supprimer un objet), si le type statique de l'opérande est différent de son type dynamique, le type statique doit être une classe de base du type dynamique de l'opérande et le type statique doit avoir un destructeur virtuel ou le comportement n'est pas défini . Dans la deuxième alternative (supprimer un tableau) si le type dynamique de l'objet à supprimer diffère de son type statique, le comportement n'est pas défini.

Et sa note de bas de page:

Cela implique qu'un objet ne peut pas être supprimé à l'aide d'un pointeur de type void * car il n'y a pas d'objets de type void

.


la source
6
Êtes-vous sûr d'avoir obtenu le bon devis? Je pense que la note de bas de page fait référence à ce texte: "Dans la première alternative (supprimer un objet), si le type statique de l'opérande est différent de son type dynamique, le type statique doit être une classe de base du type dynamique de l'opérande et le type statique le type doit avoir un destructeur virtuel ou le comportement n'est pas défini. Dans la deuxième alternative (supprimer le tableau), si le type dynamique de l'objet à supprimer diffère de son type statique, le comportement n'est pas défini. " :)
Johannes Schaub - litb
2
Vous avez raison - j'ai mis à jour la réponse. Je ne pense pas que cela annule le point de base?
Non bien sûr que non. Il dit toujours que c'est UB. Plus encore, maintenant il indique normativement que la suppression de void * est UB :)
Johannes Schaub - litb
Remplir la mémoire d'adresses pointées d'un pointeur vide avec NULLquoi faire une différence pour la gestion de la mémoire d'application?
artu-hnrq le
25

Ce n'est pas une bonne idée et pas quelque chose que vous feriez en C ++. Vous perdez vos informations de type sans raison.

Votre destructeur ne sera pas appelé sur les objets de votre tableau que vous supprimez lorsque vous l'appelez pour des types non primitifs.

Vous devez à la place remplacer nouveau / supprimer.

La suppression du void * libérera probablement votre mémoire correctement par hasard, mais c'est faux car les résultats ne sont pas définis.

Si, pour une raison inconnue de moi, vous avez besoin de stocker votre pointeur dans un vide * alors libérez-le, vous devez utiliser malloc et free.

Brian R. Bondy
la source
5
Vous avez raison de ne pas appeler le destructeur, mais vous vous trompez sur la taille inconnue. Si vous donnez à delete un pointeur que vous avez obtenu à partir de new, il connaît en fait la taille de l'objet à supprimer, entièrement indépendamment du type. Comment cela n'est-il pas spécifié par le standard C ++, mais j'ai vu des implémentations où la taille est stockée immédiatement avant les données vers lesquelles le pointeur renvoyé par 'new' pointe.
KeyserSoze le
Suppression de la partie concernant la taille, bien que la norme C ++ dise qu'elle n'est pas définie. Je sais que malloc / free fonctionnerait cependant pour les pointeurs void *.
Brian R. Bondy
Vous ne pensez pas avoir un lien vers la section pertinente de la norme? Je sais que les quelques implémentations de new / delete que j'ai examinées fonctionnent certainement correctement sans connaissance de type, mais j'avoue que je n'ai pas regardé ce que spécifie la norme. Le C ++ IIRC nécessitait à l'origine un nombre d'éléments de tableau lors de la suppression de tableaux, mais ce n'est plus le cas avec les versions les plus récentes.
KeyserSoze le
Veuillez consulter la réponse @Neil Butterworth. Sa réponse devrait être acceptée à mon avis.
Brian R. Bondy
2
@keysersoze: En général, je ne suis pas d'accord avec votre déclaration. Ce n'est pas parce qu'une implémentation a stocké la taille avant l'allocation de mémoire que c'est une règle.
13

La suppression d'un pointeur vide est dangereuse car les destructeurs ne seront pas appelés sur la valeur vers laquelle il pointe réellement. Cela peut entraîner des fuites de mémoire / ressources dans votre application.

JaredPar
la source
1
char n'a pas de constructeur / destructeur.
rxantos
9

la question n'as pas de sens. Votre confusion peut être en partie due au langage bâclé que les gens utilisent souvent avec delete:

Vous utilisez deletepour détruire un objet qui a été alloué dynamiquement. Pour ce faire, vous formez une expression de suppression avec un pointeur vers cet objet . Vous ne "supprimez jamais un pointeur". Ce que vous faites réellement, c'est "supprimer un objet identifié par son adresse".

Maintenant, nous voyons pourquoi la question n'a aucun sens: un pointeur vide n'est pas "l'adresse d'un objet". C'est juste une adresse, sans aucune sémantique. Il peut provenir de l'adresse d'un objet réel, mais cette information est perdue, car elle a été codée dans le type du pointeur d'origine. La seule façon de restaurer un pointeur d'objet est de convertir le pointeur void en un pointeur d'objet (ce qui oblige l'auteur à savoir ce que signifie le pointeur). voidlui-même est un type incomplet et donc jamais le type d'un objet, et un pointeur vide ne peut jamais être utilisé pour identifier un objet. (Les objets sont identifiés conjointement par leur type et leur adresse.)

Kerrek SB
la source
Certes, la question n'a pas beaucoup de sens sans aucun contexte environnant. Certains compilateurs C ++ compileront toujours avec plaisir ce code insensé (s'ils se sentent utiles, ils peuvent envoyer un avertissement à ce sujet). Ainsi, la question a été posée afin d'évaluer les risques connus de l'exécution de code hérité contenant cette opération peu judicieuse: va-t-il planter? fuite une partie ou la totalité de la mémoire du tableau de caractères? autre chose qui est spécifique à la plate-forme?
An̲̳̳drew
Merci pour la réponse réfléchie. Vote positif!
An̲̳̳drew
1
@Andrew: J'ai bien peur que la norme soit assez claire à ce sujet: "La valeur de l'opérande de deletepeut être une valeur de pointeur nul, un pointeur vers un objet non-tableau créé par une nouvelle expression précédente , ou un pointeur vers un sous-objet représentant une classe de base d'un tel objet. Sinon, le comportement n'est pas défini. " Donc si un compilateur accepte votre code sans diagnostic, ce n'est rien d'autre qu'un bogue dans le compilateur ...
Kerrek SB
1
@KerrekSB - Re ce n'est rien d'autre qu'un bogue dans le compilateur - je ne suis pas d'accord. La norme dit que le comportement n'est pas défini. Cela signifie que le compilateur / l'implémentation peut faire n'importe quoi et toujours être conforme à la norme. Si la réponse du compilateur est de dire que vous ne pouvez pas supprimer un pointeur void *, c'est OK. Si la réponse du compilateur est d'effacer le disque dur, c'est également OK. OTOH, si la réponse du compilateur est de ne pas générer de diagnostic mais plutôt de générer du code qui libère la mémoire associée à ce pointeur, c'est également OK. C'est une manière simple de traiter cette forme d'UB.
David Hammen
Juste pour ajouter: je ne tolère pas l'utilisation de delete void_pointer. C'est un comportement indéfini. Les programmeurs ne doivent jamais invoquer un comportement indéfini, même si la réponse semble faire ce que le programmeur voulait faire.
David Hammen
6

Si vous devez vraiment le faire, pourquoi ne pas couper l'homme du milieu (les newet deleteopérateurs) et appeler le monde operator newet operator deletedirectement? (Bien sûr, si vous essayez d'instrumenter les opérateurs newet delete, vous devez en fait réimplémenter operator newet operator delete.)

void* my_alloc (size_t size)
{
   return ::operator new(size);
}

void my_free (void* ptr)
{
   ::operator delete(ptr);
}

Notez que contrairement à malloc(), operator newjette std::bad_allocen cas d'échec (ou appelle le new_handlersi on est enregistré).

bk1e
la source
C'est correct, car char n'a pas de constructeur / destructeur.
rxantos
5

Parce que char n'a pas de logique destructrice spéciale. CECI ne fonctionnera pas.

class foo
{
   ~foo() { printf("huzza"); }
}

main()
{
   foo * myFoo = new foo();
   delete ((void*)foo);
}

L'acteur ne sera pas appelé.


la source
5

Si vous souhaitez utiliser void *, pourquoi n'utilisez-vous pas simplement malloc / free? new / delete est plus qu'une simple gestion de la mémoire. Fondamentalement, new / delete appelle un constructeur / destructeur et il se passe plus de choses. Si vous n'utilisez que des types intégrés (comme char *) et que vous les supprimez via void *, cela fonctionnerait mais ce n'est toujours pas recommandé. En fin de compte, utilisez malloc / free si vous souhaitez utiliser void *. Sinon, vous pouvez utiliser les fonctions de modèle pour votre commodité.

template<typename T>
T* my_alloc (size_t size)
{
   return new T [size];
}

template<typename T>
void my_free (T* ptr)
{
   delete [] ptr;
}

int main(void)
{
    char* pChar = my_alloc<char>(10);
    my_free(pChar);
}
Jeune
la source
Je n'ai pas écrit le code dans l'exemple - je suis tombé sur ce modèle utilisé à quelques endroits, mélangeant curieusement la gestion de la mémoire C / C ++, et je me demandais quels étaient les dangers spécifiques.
An̲̳̳drew le
L'écriture en C / C ++ est une recette d'échec. Celui qui a écrit cela aurait dû écrire l'un ou l'autre.
David Thornley
2
@David C'est du C ++, pas du C / C ++. C n'a pas de modèles, ni n'utilise new et delete.
rxantos
5

Beaucoup de gens ont déjà commenté en disant que non, il n'est pas sûr de supprimer un pointeur vide. Je suis d'accord avec cela, mais je voulais également ajouter que si vous travaillez avec des pointeurs vides afin d'allouer des tableaux contigus ou quelque chose de similaire, vous pouvez le faire avec newafin que vous puissiez utiliser en deletetoute sécurité (avec, ahem , un peu de travail supplémentaire). Cela se fait en attribuant un pointeur vide à la région mémoire (appelée «arène»), puis en fournissant le pointeur vers l'arène à nouveau. Consultez cette section dans la FAQ C ++ . Il s'agit d'une approche courante pour l'implémentation de pools de mémoire en C ++.

Paul Morie
la source
0

Il n'y a guère de raison de faire cela.

Tout d'abord, si vous ne connaissez pas le type de données, et tout ce que vous savez c'est que c'est void*, alors vous devriez vraiment simplement traiter ces données comme un blob sans type de données binaires ( unsigned char*), et utiliser malloc/ freepour les gérer . Cela est parfois nécessaire pour des éléments tels que les données de forme d'onde, etc., où vous devez transmettre des void*pointeurs vers des apis C. C'est très bien.

Si vous faites connaître le type des données (il a un cteur / dtor), mais pour une raison quelconque vous retrouviez avec un void*pointeur (pour une raison quelconque vous avez) alors vous devriez vraiment jeter de nouveau le type que vous savez à être , et invoquez- deletele.

bobobobo
la source
0

J'ai utilisé void *, (aka types inconnus) dans mon framework pendant la réflexion de code et d'autres exploits d'ambiguïté, et jusqu'à présent, je n'ai eu aucun problème (fuite de mémoire, violations d'accès, etc.) de la part des compilateurs. Uniquement les avertissements dus au fait que l'opération n'est pas standard.

Il est parfaitement logique de supprimer un inconnu (void *). Assurez-vous simplement que le pointeur suit ces instructions, sinon il peut ne plus avoir de sens:

1) Le pointeur inconnu ne doit pas pointer vers un type qui a un déconstructeur trivial, et donc lorsqu'il est converti en pointeur inconnu, il ne doit JAMAIS ÊTRE SUPPRIMÉ. Ne supprimez le pointeur inconnu qu'APRÈS l'avoir renvoyé dans le type ORIGINAL.

2) L'instance est-elle référencée comme un pointeur inconnu dans la mémoire liée à la pile ou au tas? Si le pointeur inconnu fait référence à une instance sur la pile, il ne doit JAMAIS ÊTRE SUPPRIMÉ!

3) Êtes-vous sûr à 100% que le pointeur inconnu est une région de mémoire valide? Non, alors il ne devrait JAMAIS ÊTRE SUPPRIMÉ!

En tout, il y a très peu de travail direct qui peut être fait en utilisant un type de pointeur inconnu (void *). Cependant, indirectement, le void * est un excellent atout sur lequel les développeurs C ++ peuvent s'appuyer lorsque l'ambiguïté des données est requise.

zackery.fix
la source
0

Si vous voulez juste un tampon, utilisez malloc / free. Si vous devez utiliser new / delete, considérez une classe wrapper triviale:

template<int size_ > struct size_buffer { 
  char data_[ size_]; 
  operator void*() { return (void*)&data_; }
};

typedef sized_buffer<100> OpaqueBuffer; // logical description of your sized buffer

OpaqueBuffer* ptr = new OpaqueBuffer();

delete ptr;
Sanjaya R
la source
0

Pour le cas particulier de char.

char est un type intrinsèque qui n'a pas de destructeur spécial. Les arguments relatifs aux fuites sont donc sans objet.

sizeof (char) est généralement un, donc il n'y a pas non plus d'argument d'alignement. Dans le cas d'une plate-forme rare où le sizeof (char) n'est pas un, ils allouent une mémoire suffisamment alignée pour leur char. L'argument de l'alignement est donc également théorique.

malloc / free serait plus rapide dans ce cas. Mais vous perdez std :: bad_alloc et devez vérifier le résultat de malloc. Il serait peut-être préférable d'appeler les opérateurs globaux new et delete car cela contournerait l'intermédiaire.

rxantos
la source
1
" sizeof (char) est généralement un " sizeof (char) est TOUJOURS un
curiousguy
Ce n'est que récemment (2019) que les gens pensent que c'est newréellement défini pour lancer. Ce n'est pas vrai. Il dépend du compilateur et du commutateur du compilateur. Voir par exemple les /GX[-] enable C++ EH (same as /EHsc)commutateurs MSVC2019 . De plus, sur les systèmes embarqués, beaucoup choisissent de ne pas payer les taxes de performance pour les exceptions C ++. Donc la phrase commençant par "Mais vous perdez std :: bad_alloc ..." est discutable.
BitTickler