J'ai trouvé du code en utilisant std :: shared_ptr pour effectuer un nettoyage arbitraire à l'arrêt. Au début, je pensais que ce code ne pouvait pas fonctionner, mais j'ai ensuite essayé ce qui suit:
#include <memory>
#include <iostream>
#include <vector>
class test {
public:
test() {
std::cout << "Test created" << std::endl;
}
~test() {
std::cout << "Test destroyed" << std::endl;
}
};
int main() {
std::cout << "At begin of main.\ncreating std::vector<std::shared_ptr<void>>"
<< std::endl;
std::vector<std::shared_ptr<void>> v;
{
std::cout << "Creating test" << std::endl;
v.push_back( std::shared_ptr<test>( new test() ) );
std::cout << "Leaving scope" << std::endl;
}
std::cout << "Leaving main" << std::endl;
return 0;
}
Ce programme donne la sortie:
At begin of main.
creating std::vector<std::shared_ptr<void>>
Creating test
Test created
Leaving scope
Leaving main
Test destroyed
J'ai quelques idées sur les raisons pour lesquelles cela pourrait fonctionner, qui ont à voir avec les composants internes de std :: shared_ptrs tels qu'implémentés pour G ++. Étant donné que ces objets encapsulent le pointeur interne avec le compteur, la conversion de std::shared_ptr<test>
à std::shared_ptr<void>
ne gêne probablement pas l'appel du destructeur. Cette hypothèse est-elle correcte?
Et bien sûr, la question beaucoup plus importante: est-ce que cela fonctionne avec la norme, ou des modifications supplémentaires apportées aux composants internes de std :: shared_ptr, d'autres implémentations peuvent-elles réellement casser ce code?
la source
Réponses:
L'astuce consiste à
std::shared_ptr
effectuer un effacement de type. Fondamentalement, quand un nouveaushared_ptr
est créé, il stockera en interne unedeleter
fonction (qui peut être donnée comme argument au constructeur mais si elle n'est pas présente par défaut à l'appeldelete
). Lorsque leshared_ptr
est détruit, il appelle cette fonction stockée et cela appellera ledeleter
.Une simple esquisse de l'effacement de type simplifié avec std :: function et évitant tout comptage de références et autres problèmes peut être vue ici:
Quand a
shared_ptr
est copié (ou construit par défaut) à partir d'un autre, le suppresseur est transmis, de sorte que lorsque vous construisez a àshared_ptr<T>
partir de a,shared_ptr<U>
les informations sur le destructeur à appeler sont également transmises dans le fichierdeleter
.la source
my_shared
. Je corrigerais cela mais je n'ai pas encore le privilège de modifier.std::shared_ptr<void>
me permet d'éviter de déclarer une classe wrapper inutile juste pour pouvoir l'hériter d'une certaine classe de base.my_unique_ptr
. Lorsque dansmain
le modèle est instancié avecdouble
le bon suppresseur est choisi mais cela ne fait pas partie du type demy_unique_ptr
et ne peut pas être récupéré à partir de l'objet. Le type du suppresseur est effacé de l'objet, lorsqu'une fonction reçoit unmy_unique_ptr
(disons par rvalue-reference), cette fonction ne le sait pas et n'a pas besoin de savoir ce qu'est le suppresseur.shared_ptr<T>
logiquement, [*] a (au moins) deux membres de données pertinents:La fonction de suppression de votre
shared_ptr<Test>
, étant donné la manière dont vous l'avez construite, est la fonction normale pourTest
, qui convertit le pointeur enTest*
etdelete
s.Lorsque vous poussez votre
shared_ptr<Test>
dans le vecteur deshared_ptr<void>
, les deux sont copiés, bien que le premier soit converti envoid*
.Ainsi, lorsque l'élément vectoriel est détruit en prenant la dernière référence avec lui, il passe le pointeur vers un déléteur qui le détruit correctement.
C'est en fait un peu plus compliqué que ça, car
shared_ptr
peut prendre un foncteur de suppression plutôt qu'une simple fonction, donc il peut même y avoir des données par objet à stocker plutôt qu'un simple pointeur de fonction. Mais dans ce cas, il n'y a pas de telles données supplémentaires, il suffirait simplement de stocker un pointeur vers une instanciation d'une fonction de modèle, avec un paramètre de modèle qui capture le type par lequel le pointeur doit être supprimé.[*] logiquement dans le sens où il y a accès - ils peuvent ne pas être membres du shared_ptr lui-même mais au lieu d'un nœud de gestion vers lequel il pointe.
la source
shared_ptr
directement avec le type approprié ou si vous utilisezmake_shared
. Mais, il est encore une bonne idée que le type du pointeur peut changer de construction jusqu'à ce qu'il soit stocké dans leshared_ptr
:base *p = new derived; shared_ptr<base> sp(p);
, en ce quishared_ptr
est concerne l'objet estbase
pasderived
, vous avez donc besoin d' un destructeur virtuel. Ce modèle peut être commun aux modèles d'usine, par exemple.Cela fonctionne car il utilise l'effacement de type.
Fondamentalement, lorsque vous construisez un
shared_ptr
, il passe un argument supplémentaire (que vous pouvez réellement fournir si vous le souhaitez), qui est le foncteur de suppression.Ce foncteur par défaut accepte comme argument un pointeur sur le type que vous utilisez dans le
shared_ptr
, doncvoid
ici, le convertit de manière appropriée au type statique que vous avez utilisétest
ici, et appelle le destructeur sur cet objet.Toute science suffisamment avancée ressemble à de la magie, n'est-ce pas?
la source
Le constructeur
shared_ptr<T>(Y *p)
semble en effet appelershared_ptr<T>(Y *p, D d)
whered
est un suppresseur généré automatiquement pour l'objet.Lorsque cela se produit, le type de l'objet
Y
est connu, ainsi le suppresseur de cetshared_ptr
objet sait quel destructeur appeler et cette information n'est pas perdue lorsque le pointeur est stocké dans un vecteur deshared_ptr<void>
.En effet, les spécifications exigent que pour qu'un
shared_ptr<T>
objet recevant accepte unshared_ptr<U>
objet, il doit être vrai que etU*
doit être implicitement convertible en aT*
et c'est certainement le cas avecT=void
parce que tout pointeur peut être converti en avoid*
implicitement. Rien n'est dit sur le suppresseur qui sera invalide, donc en effet, les spécifications exigent que cela fonctionnera correctement.Techniquement, IIRC a
shared_ptr<T>
contient un pointeur vers un objet caché qui contient le compteur de référence et un pointeur vers l'objet réel; en stockant le suppresseur dans cette structure cachée, il est possible de faire fonctionner cette fonctionnalité apparemment magique tout en gardantshared_ptr<T>
la taille d'un pointeur normal (cependant, le déréférencement du pointeur nécessite une double indirectionla source
Test*
est implicitement convertible envoid*
, doncshared_ptr<Test>
implicitement convertible enshared_ptr<void>
, depuis la mémoire. Cela fonctionne car ilshared_ptr
est conçu pour contrôler la destruction au moment de l'exécution, pas au moment de la compilation, ils utiliseront en interne l'héritage pour appeler le destructeur approprié tel qu'il était au moment de l'allocation.la source
Je vais répondre à cette question (2 ans plus tard) en utilisant une implémentation très simpliste de shared_ptr que l'utilisateur comprendra.
Tout d'abord, je vais à quelques classes secondaires, shared_ptr_base, sp_counted_base sp_counted_impl et checked_deleter, dont la dernière est un modèle.
Maintenant, je vais créer deux fonctions "gratuites" appelées make_sp_counted_impl qui renverront un pointeur vers une fonction nouvellement créée.
Ok, ces deux fonctions sont essentielles pour ce qui va se passer ensuite lorsque vous créez un shared_ptr via une fonction basée sur un modèle.
Notez ce qui se passe ci-dessus si T est nul et U est votre classe "test". Il appellera make_sp_counted_impl () avec un pointeur vers U, pas un pointeur vers T. La gestion de la destruction se fait par ici. La classe shared_ptr_base gère le comptage des références en ce qui concerne la copie et l'affectation, etc. La classe shared_ptr elle-même gère l'utilisation de type sécurisé des surcharges d'opérateurs (->, * etc).
Ainsi, bien que vous ayez un shared_ptr à void, vous gérez en dessous un pointeur du type que vous avez passé dans new. Notez que si vous convertissez votre pointeur en void * avant de le mettre dans shared_ptr, il ne parviendra pas à se compiler sur le checked_delete, vous êtes donc en sécurité là aussi.
la source