J'ai commencé à étudier les pointeurs intelligents de C++ 11 et je ne vois aucune utilisation utile de std::weak_ptr
. Quelqu'un peut-il me dire quand std::weak_ptr
est utile/nécessaire?
Un bon exemple serait un cache.
Pour les objets récemment accédés, vous souhaitez les conserver en mémoire, vous devez donc conserver un pointeur fort sur eux. Périodiquement, vous analysez le cache et décidez quels objets n'ont pas été consultés récemment. Vous n'avez pas besoin de les garder en mémoire pour vous débarrasser du pointeur fort.
Mais que se passe-t-il si cet objet est en cours d'utilisation et qu'un autre code contient un pointeur fort? Si le cache se débarrasse de son seul pointeur sur l'objet, il ne pourra plus jamais le retrouver. Ainsi, le cache conserve un pointeur faible sur les objets qu’il doit détecter s’ils restent en mémoire.
C'est exactement ce que fait un pointeur faible: il vous permet de localiser un objet s'il est toujours là, mais ne le garde pas si rien d'autre n'en a besoin.
std::weak_ptr
est un très bon moyen de résoudre le problème pointeur en suspens . En utilisant simplement des pointeurs bruts, il est impossible de savoir si les données référencées ont été désallouées ou non. Au lieu de cela, en laissant un std::shared_ptr
gérer les données et en fournissant std::weak_ptr
aux utilisateurs des données, ceux-ci peuvent vérifier la validité des données en appelant expired()
ou lock()
.
Vous ne pouvez pas faire cela avec std::shared_ptr
seul, car toutes les instances de std::shared_ptr
partagent la propriété des données qui ne sont pas supprimées avant que toutes les instances de std::shared_ptr
ne soient supprimées. Voici un exemple de vérification du pointeur suspendu avec lock()
:
#include <iostream>
#include <memory>
int main()
{
// OLD, problem with dangling pointer
// PROBLEM: ref will point to undefined data!
int* ptr = new int(10);
int* ref = ptr;
delete ptr;
// NEW
// SOLUTION: check expired() or lock() to determine if pointer is valid
// empty definition
std::shared_ptr<int> sptr;
// takes ownership of pointer
sptr.reset(new int);
*sptr = 10;
// get pointer to data without taking ownership
std::weak_ptr<int> weak1 = sptr;
// deletes managed object, acquires new pointer
sptr.reset(new int);
*sptr = 5;
// get pointer to new data without taking ownership
std::weak_ptr<int> weak2 = sptr;
// weak1 is expired!
if(auto tmp = weak1.lock())
std::cout << *tmp << '\n';
else
std::cout << "weak1 is expired\n";
// weak2 points to new data (5)
if(auto tmp = weak2.lock())
std::cout << *tmp << '\n';
else
std::cout << "weak2 is expired\n";
}
Une autre réponse, espérons-le plus simple. (pour les autres googlers)
Supposons que vous ayez les objets Team
et Member
.
Évidemment, c'est une relation: l'objet Team
aura des pointeurs sur sa Members
. Et il est probable que les membres auront également un pointeur arrière sur leur objet Team
.
Ensuite, vous avez un cycle de dépendance. Si vous utilisez shared_ptr
, les objets ne seront plus automatiquement libérés lorsque vous y abandonnerez une référence, car ils se référencent les uns les autres de manière cyclique. C'est une fuite de mémoire.
Vous cassez ceci en utilisant weak_ptr
. Le "propriétaire" utilise généralement shared_ptr
et le "propriétaire" utilise un weak_ptr
en son parent et le convertit temporairement en shared_ptr
lorsqu'il doit accéder à son parent.
Stocker un ptr faible:
weak_ptr<Parent> parentWeakPtr_ = parentSharedPtr; // automatic conversion to weak from shared
puis utilisez-le quand vous en aurez besoin
shared_ptr<Parent> tempParentSharedPtr = parentWeakPtr_.lock(); // on the stack, from the weak ptr
if( !tempParentSharedPtr ) {
// yes it may failed if parent was freed since we stored weak_ptr
} else {
// do stuff
}
// tempParentSharedPtr is released when it goes out of scope
Voici un exemple donné par @jleahy: Supposons que vous ayez une collection de tâches exécutées de manière asynchrone et gérées par un std::shared_ptr<Task>
. Vous voudrez peut-être faire quelque chose avec ces tâches régulièrement, ainsi un événement de minuterie peut traverser un std::vector<std::weak_ptr<Task>>
et donner aux tâches quelque chose à faire. Cependant, une tâche peut avoir simultanément décidé qu’elle n’était plus nécessaire et mourrait. Le minuteur peut ainsi vérifier si la tâche est toujours active en créant un pointeur partagé à partir du pointeur faible et en utilisant ce pointeur partagé, à condition qu'il ne soit pas nul.
Ils sont utiles avec Boost.Asio lorsqu'il n'est pas garanti qu'un objet cible existe toujours lorsqu'un gestionnaire asynchrone est appelé. L'astuce consiste à lier un weak_ptr
à l'objet gestionnaire asynchrone, à l'aide de std::bind
ou de captures lambda.
void MyClass::startTimer()
{
std::weak_ptr<MyClass> weak = shared_from_this();
timer_.async_wait( [weak](const boost::system::error_code& ec)
{
auto self = weak.lock();
if (self)
{
self->handleTimeout();
}
else
{
std::cout << "Target object no longer exists!\n";
}
} );
}
Il s’agit d’une variante du langage self = shared_from_this()
fréquemment observée dans les exemples Boost.Asio, dans laquelle un gestionnaire asynchrone en attente not prolonge la durée de vie de l’objet cible tout en restant safe si l'objet cible est supprimé.
weak_ptr
est également utile pour vérifier la suppression correcte d'un objet - en particulier dans les tests unitaires. Le cas d'utilisation typique pourrait ressembler à ceci:
std::weak_ptr<X> weak_x{ shared_x };
shared_x.reset();
BOOST_CHECK(weak_x.lock());
... //do something that should remove all other copies of shared_x and hence destroy x
BOOST_CHECK(!weak_x.lock());
shared_ptr : contient l'objet réel.
faible_ptr : utilise lock
pour se connecter au propriétaire réel ou renvoie NULL sinon.
Grosso modo, le rôle de weak_ptr
est similaire à celui de agence de logement . Sans agents, pour louer une maison, il se peut que nous devions vérifier des maisons au hasard dans la ville. Les agents s’assurent que nous ne visitons que les maisons qui sont toujours accessibles et disponibles à la location.
Lors de l'utilisation de pointeurs, il est important de comprendre les différents types de pointeurs disponibles et quand il est judicieux de les utiliser. Il existe quatre types de pointeurs dans les deux catégories suivantes:
SomeClass* ptrToSomeClass = new SomeClass();
]std::unique_ptr<SomeClass> uniquePtrToSomeClass ( new SomeClass() );
std::shared_ptr<SomeClass> sharedPtrToSomeClass ( new SomeClass() );
std::weak_ptr<SomeClass> weakPtrToSomeWeakOrSharedPtr ( weakOrSharedPtr );
Les pointeurs bruts (parfois appelés "pointeurs hérités" ou "pointeurs C") fournissent un comportement de pointeur "sans système" et constituent une source courante de bogues et de fuites de mémoire. Les pointeurs bruts ne fournissent aucun moyen de garder la trace de la propriété de la ressource et les développeurs doivent appeler "supprimer" manuellement pour s'assurer qu'ils ne créent pas de fuite de mémoire. Cela devient difficile si la ressource est partagée, car il peut être difficile de savoir si des objets pointent toujours sur la ressource. Pour ces raisons, les pointeurs bruts doivent généralement être évités et utilisés uniquement dans les sections du code critiques en termes de performances et ayant une portée limitée.
Les pointeurs uniques sont un pointeur intelligent de base qui "possède" le pointeur brut sous-jacent sur la ressource et est responsable de l'appel de delete et de la libération de la mémoire allouée une fois que l'objet qui "possède" le pointeur unique est hors de portée. Le nom "unique" fait référence au fait qu'un seul objet peut "posséder" le pointeur unique à un moment donné. La propriété peut être transférée à un autre objet via la commande move, mais un pointeur unique ne peut jamais être copié ou partagé. Pour ces raisons, les pointeurs uniques constituent une bonne alternative aux pointeurs bruts dans le cas où un seul objet a besoin du pointeur à un moment donné, ce qui évite au développeur de libérer de la mémoire à la fin du cycle de vie de l'objet propriétaire.
Les pointeurs partagés sont un autre type de pointeur intelligent qui s'apparentent à des pointeurs uniques, mais permettent à de nombreux objets de devenir propriétaires du pointeur partagé. Comme les pointeurs uniques, les pointeurs partagés sont responsables de la libération de la mémoire allouée une fois que tous les objets sont dirigés vers la ressource. Pour ce faire, il utilise une technique appelée comptage de références. Chaque fois qu'un nouvel objet devient propriétaire du pointeur partagé, le nombre de références est incrémenté de un. De même, lorsqu'un objet sort de la portée ou cesse de pointer vers la ressource, le compte de références est décrémenté de un. Lorsque le compte de références atteint zéro, la mémoire allouée est libérée. Pour ces raisons, les pointeurs partagés sont un type très puissant de pointeur intelligent qui doit être utilisé chaque fois que plusieurs objets doivent pointer vers la même ressource.
Enfin, les pointeurs faibles sont un autre type de pointeur intelligent qui, plutôt que de pointer directement sur une ressource, pointent sur un autre pointeur (faible ou partagé). Les pointeurs faibles ne peuvent pas accéder directement à un objet, mais ils peuvent dire si l'objet existe toujours ou s'il a expiré. Un pointeur faible peut être temporairement converti en un pointeur partagé pour accéder à l'objet pointé (à condition qu'il existe toujours). Pour illustrer cela, considérons l'exemple suivant:
Dans l'exemple, vous avez un pointeur faible sur la réunion B. Vous n'êtes pas un "propriétaire" dans la réunion B; il peut donc se terminer sans vous et vous ne savez pas s'il s'est terminé ou non, à moins que vous ne vérifiiez. S'il n'est pas terminé, vous pouvez vous inscrire et participer, sinon, vous ne pouvez pas. Cela diffère de la présence d'un pointeur partagé sur la réunion B, car vous seriez alors un "propriétaire" à la fois dans la réunion A et dans la réunion B (en participant aux deux en même temps).
L'exemple montre comment un pointeur faible fonctionne et est utile lorsqu'un objet doit être un extérieur observateur, mais ne veut pas la responsabilité du partage de la propriété. Ceci est particulièrement utile dans le cas où deux objets doivent se désigner (par exemple, une référence circulaire). Avec les pointeurs partagés, aucun objet ne peut être libéré car ils sont toujours "fortement" pointés par l'autre objet. Lorsqu'un des pointeurs est un pointeur faible, l'objet contenant le pointeur faible peut toujours accéder à l'autre objet en cas de besoin, à condition qu'il existe toujours.
Outre les autres cas d'utilisation valides déjà mentionnés, std::weak_ptr
est un outil formidable dans un environnement multithread, car
std::shared_ptr
associé à std::weak_ptr
est protégé contre les pointeurs en suspens - contrairement à std::unique_ptr
en association avec des pointeurs brutsstd::weak_ptr::lock()
est une opération atomique (voir aussi À propos de la sécurité des threads de faible_ptr )Envisagez une tâche pour charger toutes les images d’un répertoire (~ 10 000) simultanément dans la mémoire (par exemple sous la forme d’un cache de vignettes). Évidemment, la meilleure façon de le faire est un thread de contrôle, qui gère et gère les images, et plusieurs threads de travail, qui chargent les images. Maintenant, c'est une tâche facile. Voici une implémentation très simplifiée (join()
etc est omis, les threads devraient être gérés différemment dans une implémentation réelle, etc.)
// a simplified class to hold the thumbnail and data
struct ImageData {
std::string path;
std::unique_ptr<YourFavoriteImageLibData> image;
};
// a simplified reader fn
void read( std::vector<std::shared_ptr<ImageData>> imagesToLoad ) {
for( auto& imageData : imagesToLoad )
imageData->image = YourFavoriteImageLib::load( imageData->path );
}
// a simplified manager
class Manager {
std::vector<std::shared_ptr<ImageData>> m_imageDatas;
std::vector<std::unique_ptr<std::thread>> m_threads;
public:
void load( const std::string& folderPath ) {
std::vector<std::string> imagePaths = readFolder( folderPath );
m_imageDatas = createImageDatas( imagePaths );
const unsigned numThreads = std::thread::hardware_concurrency();
std::vector<std::vector<std::shared_ptr<ImageData>>> splitDatas =
splitImageDatas( m_imageDatas, numThreads );
for( auto& dataRangeToLoad : splitDatas )
m_threads.Push_back( std::make_unique<std::thread>(read, dataRangeToLoad) );
}
};
Mais cela devient beaucoup plus compliqué si vous souhaitez interrompre le chargement des images, par exemple. parce que l'utilisateur a choisi un répertoire différent. Ou même si vous voulez détruire le manager.
Vous auriez besoin d'une communication entre les threads et devez arrêter tous les threads du chargeur avant de pouvoir modifier votre champ m_imageDatas
. Sinon, les chargeurs continueraient à charger jusqu'à ce que toutes les images soient terminées, même si elles sont déjà obsolètes. Dans l'exemple simplifié, cela ne serait pas trop difficile, mais dans un environnement réel, les choses peuvent être beaucoup plus compliquées.
Les threads feraient probablement partie d'un pool de threads utilisé par plusieurs gestionnaires, dont certains sont arrêtés, d'autres ne le sont pas, etc. Le paramètre simple imagesToLoad
serait une file d'attente verrouillée, dans laquelle ces gestionnaires transmettent leur image. requêtes provenant de différents threads de contrôle, les lecteurs les affichant - dans un ordre arbitraire - à l’autre extrémité. Ainsi, la communication devient difficile, lente et sujette aux erreurs. Un moyen très élégant d'éviter toute communication supplémentaire dans de tels cas consiste à utiliser std::shared_ptr
conjointement avec std::weak_ptr
.
// a simplified reader fn
void read( std::vector<std::weak_ptr<ImageData>> imagesToLoad ) {
for( auto& imageDataWeak : imagesToLoad ) {
std::shared_ptr<ImageData> imageData = imageDataWeak.lock();
if( !imageData )
continue;
imageData->image = YourFavoriteImageLib::load( imageData->path );
}
}
// a simplified manager
class Manager {
std::vector<std::shared_ptr<ImageData>> m_imageDatas;
std::vector<std::unique_ptr<std::thread>> m_threads;
public:
void load( const std::string& folderPath ) {
std::vector<std::string> imagePaths = readFolder( folderPath );
m_imageDatas = createImageDatas( imagePaths );
const unsigned numThreads = std::thread::hardware_concurrency();
std::vector<std::vector<std::weak_ptr<ImageData>>> splitDatas =
splitImageDatasToWeak( m_imageDatas, numThreads );
for( auto& dataRangeToLoad : splitDatas )
m_threads.Push_back( std::make_unique<std::thread>(read, dataRangeToLoad) );
}
};
Cette implémentation est presque aussi simple que la première, ne nécessite aucune communication de thread supplémentaire et peut faire partie d’un pool de threads/file d’attente dans une implémentation réelle. Étant donné que les images expirées sont ignorées et que les images non expirées sont traitées, les threads ne devraient jamais être arrêtés en mode de fonctionnement normal. Vous pouvez toujours modifier le chemin en toute sécurité ou détruire vos gestionnaires, car le lecteur vérifie si le pointeur propriétaire n’est pas expiré.
Quand on ne veut pas posséder l'objet:
Ex:
class A
{
shared_ptr<int> sPtr1;
weak_ptr<int> wPtr1;
}
Dans la classe ci-dessus, wPtr1 ne possède pas la ressource indiquée par wPtr1. Si la ressource est supprimée, alors wPtr1 est expiré.
Pour éviter la dépendance circulaire:
shard_ptr<A> <----| shared_ptr<B> <------
^ | ^ |
| | | |
| | | |
| | | |
| | | |
class A | class B |
| | | |
| ------------ |
| |
-------------------------------------
Maintenant, si nous faisons le shared_ptr de la classe B et A, le use_count du pointeur est deux.
Lorsque le shared_ptr s'éteint od scope, le nombre reste toujours égal à 1 et les objets A et B ne sont donc pas supprimés.
class B;
class A
{
shared_ptr<B> sP1; // use weak_ptr instead to avoid CD
public:
A() { cout << "A()" << endl; }
~A() { cout << "~A()" << endl; }
void setShared(shared_ptr<B>& p)
{
sP1 = p;
}
};
class B
{
shared_ptr<A> sP1;
public:
B() { cout << "B()" << endl; }
~B() { cout << "~B()" << endl; }
void setShared(shared_ptr<A>& p)
{
sP1 = p;
}
};
int main()
{
shared_ptr<A> aPtr(new A);
shared_ptr<B> bPtr(new B);
aPtr->setShared(bPtr);
bPtr->setShared(aPtr);
return 0;
}
sortie:
A()
B()
Comme nous pouvons le voir à la sortie, les pointeurs A et B ne sont jamais supprimés et par conséquent une fuite de mémoire.
Pour éviter ce problème, utilisez simplement faible_ptr dans la classe A au lieu de shared_ptr, ce qui est plus logique.
http://fr.cppreference.com/w/cpp/memory/weak_ptr std :: faible_ptr est un pointeur intelligent qui contient une référence non propriétaire ("faible") à un objet géré. par std :: shared_ptr. Il doit être converti en std :: shared_ptr pour pouvoir accéder à l'objet référencé.
std :: faible_ptr modélise la propriété temporaire: lorsqu'un objet doit être accédé uniquement s'il existe et qu'il peut être supprimé à tout moment par quelqu'un d'autre, std :: faible_ptr est utilisé pour suivre l'objet et converti en std: : shared_ptr à prendre en charge temporairement. Si le std :: shared_ptr d'origine est détruit à ce stade, la durée de vie de l'objet est prolongée jusqu'à ce que le std :: shared_ptr temporaire soit également détruit.
De plus, std :: faible_ptr est utilisé pour rompre les références circulaires de std :: shared_ptr.
Le pointeur partagé présente un inconvénient: shared_pointer ne peut pas gérer la dépendance du cycle parent-enfant. Indique si la classe parent utilise l'objet de la classe enfant à l'aide d'un pointeur partagé, dans le même fichier si la classe enfant utilise l'objet de la classe parent. Le pointeur partagé ne parviendra pas à détruire tous les objets, même le pointeur partagé n'appelle pas du tout le destructeur dans le scénario de dépendance de cycle. le pointeur fondamentalement partagé ne prend pas en charge le mécanisme de décompte de références.
Cet inconvénient, nous pouvons surmonter en utilisant faible_pointer.
Je vois std::weak_ptr<T>
comme un traitement vers un std::shared_ptr<T>
: Cela me permet d'obtenir le std::shared_ptr<T>
s'il reste existe, mais il ne prolongera pas sa durée de vie. Il existe plusieurs scénarios lorsqu'un tel point de vue est utile:
// Some sort of image; very expensive to create.
std::shared_ptr< Texture > texture;
// A Widget should be able to quickly get a handle to a Texture. On the
// other hand, I don't want to keep Textures around just because a widget
// may need it.
struct Widget {
std::weak_ptr< Texture > texture_handle;
void render() {
if (auto texture = texture_handle.get(); texture) {
// do stuff with texture. Warning: `texture`
// is now extending the lifetime because it
// is a std::shared_ptr< Texture >.
} else {
// gracefully degrade; there's no texture.
}
}
};
Un autre scénario important consiste à rompre les cycles dans les structures de données.
// Asking for trouble because a node owns the next node, and the next node owns
// the previous node: memory leak; no destructors automatically called.
struct Node {
std::shared_ptr< Node > next;
std::shared_ptr< Node > prev;
};
// Asking for trouble because a parent owns its children and children own their
// parents: memory leak; no destructors automatically called.
struct Node {
std::shared_ptr< Node > parent;
std::shared_ptr< Node > left_child;
std::shared_ptr< Node > right_child;
};
// Better: break dependencies using a std::weak_ptr (but not best way to do it;
// see Herb Sutter's talk).
struct Node {
std::shared_ptr< Node > next;
std::weak_ptr< Node > prev;
};
// Better: break dependencies using a std::weak_ptr (but not best way to do it;
// see Herb Sutter's talk).
struct Node {
std::weak_ptr< Node > parent;
std::shared_ptr< Node > left_child;
std::shared_ptr< Node > right_child;
};
Herb Sutter a un excellent discours qui explique la meilleure utilisation des fonctionnalités du langage (dans ce cas des pointeurs intelligents) pour assurer la liberté de fuite par défaut (signifiant: tout se met en place par construction, vous pouvez difficilement le bousiller). C'est une montre à ne pas manquer.