Considérez l'implémentation de liste liée individuellement suivante:
struct node {
std::unique_ptr<node> next;
ComplicatedDestructorClass data;
}
Supposons maintenant que j'arrête d'utiliser une std::unique_ptr<node> head
instance qui sort alors du champ d'application, provoquant l'appel de son destructeur.
Est-ce que cela fera exploser ma pile de listes suffisamment grandes? Est-il juste de supposer que le compilateur fera une optimisation assez compliquée (le unique_ptr
destructeur en ligne dans node
le s, puis utilisera la récursivité de la queue), ce qui devient beaucoup plus difficile si je fais ce qui suit (puisque le data
destructeur masquerait next
le sien , le rendant difficile pour que le compilateur remarque la possibilité de réorganisation et d'appel de queue):
struct node {
std::shared_ptr<node> next;
ComplicatedDestructorClass data;
}
Si en data
quelque sorte a un pointeur vers son, node
il peut même être impossible pour la récursivité de la queue (bien que nous devrions bien sûr nous efforcer d'éviter de telles violations de l'encapsulation).
En général, comment est-il censé détruire cette liste sinon? Nous ne pouvons pas parcourir la liste et supprimer le nœud "actuel" car le pointeur partagé n'en a pas release
! Le seul moyen est avec un deleter personnalisé, qui me fait vraiment mal.
gcc -O3
n'a pas pu optimiser une récursivité de queue (dans un exemple compliqué).Réponses:
Oui, cela finira par faire exploser votre pile, à moins que le compilateur ne se trouve à appliquer une optimisation d'appel de queue au
node
destructeur etshared_ptr
au destructeur de. Ce dernier est extrêmement dépendant de l'implémentation de bibliothèque standard. La STL de Microsoft, par exemple, ne le fera jamais, carshared_ptr
elle diminue d'abord le nombre de références de sa pointe (peut-être en détruisant l'objet), puis diminue le nombre de références de son bloc de contrôle (le faible nombre de références). Le destructeur intérieur n'est donc pas un appel de queue. Il s'agit également d'un appel virtuel , ce qui rend encore moins probable son optimisation.Les listes typiques contournent ce problème en n'ayant pas un nœud propre au suivant, mais en ayant un conteneur qui possède tous les nœuds et utilise une boucle pour supprimer tout dans le destructeur.
la source
shared_ptr
s à la fin. Je ne peux pas me débarrasser complètement des pointeurs car j'avais besoin de la sécurité du fil.std::atomic_*
surcharges pour eux, non?std::atomic<node*>
aussi, et moins cher.Réponse tardive mais puisque personne ne l'a fournie ... J'ai rencontré le même problème et l'ai résolu en utilisant un destructeur personnalisé:
Si vous avez vraiment une liste , c'est-à-dire que chaque nœud est précédé d'un nœud et a au plus un suiveur, et que vous êtes
list
un pointeur sur le premiernode
, ce qui précède devrait fonctionner.Si vous avez une structure floue (par exemple un graphique acyclique), vous pouvez utiliser ce qui suit:
L'idée est que lorsque vous faites:
L'ancien pointeur partagé
next
est détruit (car il l'use_count
est maintenant0
) et vous pointez sur ce qui suit. Cela fait exactement la même chose que le destructeur par défaut, sauf qu'il le fait de manière itérative plutôt que récursive et évite ainsi le débordement de pile.la source
next = std::move(next->next)
appelnext->~node()
récursif.next->next
est invalidé (par l'opérateur d'affectation de déplacement) avant que la valeur pointée par nenext
soit détruite, "arrêtant" ainsi la récursivité. J'utilise en fait ce code et ce travail (testé avecg++
,clang
etmsvc
), mais maintenant que vous le dites, je ne suis pas sûr que cela soit défini par la norme (le fait que le pointeur déplacé soit invalidé avant la destruction de l'ancien objet pointé par le pointeur cible).operator=(std::shared_ptr&& r)
est équivalent àstd::shared_ptr(std::move(r)).swap(*this)
. Toujours à partir de la norme, le constructeur de déplacement destd::shared_ptr(std::shared_ptr&& r)
faitr
vide,r
est donc vide (r.get() == nullptr
) avant l'appel àswap
. Dans mon cas, ce moyennext->next
est vide avant que l'ancien objet pointé par nenext
soit détruit (par l'swap
appel).f
est activénext
, nonnext->next
, et puisqu'ilnext->next
est nul, il s'arrête immédiatement.Pour être honnête, je ne connais pas l'algorithme de désallocation de pointeur intelligent d'aucun compilateur C ++, mais je peux imaginer un algorithme simple et non récursif qui fait cela. Considère ceci:
Par conséquent, il n'y aurait aucune chance pour que la pile déborde, et c'est beaucoup plus simple que d'optimiser un algorithme récursif.
Je ne sais pas si cela correspond à la philosophie des «pointeurs intelligents à coût presque nul».
Je suppose que ce que vous avez décrit ne causerait pas de débordement de pile, mais vous pourriez essayer de construire une expérience intelligente pour me prouver le contraire.
MISE À JOUR
Eh bien, cela prouve mal ce que j'ai écrit précédemment:
Ce programme construit éternellement et déconstruit une chaîne de nœuds. Cela provoque un débordement de pile.
la source