En ce qui concerne les pointeurs intelligents et les nouvelles fonctionnalités C++ 11/14, je me demande quelles seraient les meilleures valeurs de retour et les types de paramètres de fonction pour les classes qui ont ces fonctionnalités:
Fonction d'usine (en dehors de la classe) qui crée des objets et les renvoie aux utilisateurs de la classe. (Par exemple, ouvrir un document et renvoyer un objet qui peut être utilisé pour accéder au contenu.)
Les fonctions utilitaires qui acceptent les objets des fonctions d'usine, les utilisent, mais n'en deviennent pas propriétaires. (Par exemple, une fonction qui compte le nombre de mots dans le document.)
Fonctions qui gardent une référence à l'objet après leur retour (comme un composant d'interface utilisateur qui prend une copie de l'objet afin qu'il puisse dessiner le contenu à l'écran selon les besoins.)
Quel serait le meilleur type de retour pour la fonction d'usine?
delete
correctement, ce qui est problématique.unique_ptr<>
alors l'utilisateur ne peut pas le partager s'il le souhaite.shared_ptr<>
alors devrai-je faire circuler shared_ptr<>
types partout? C'est ce que je fais maintenant et cela pose des problèmes car j'obtiens des références cycliques, empêchant les objets d'être détruits automatiquement.Quel est le meilleur type de paramètre pour la fonction utilitaire?
const
pour indiquer clairement que la fonction ne modifiera pas l'objet, sans casser la compatibilité du pointeur intelligent?Quel est le meilleur type de paramètre pour la fonction qui garde une référence à l'objet?
shared_ptr<>
est la seule option ici, ce qui signifie probablement que la classe d'usine doit renvoyer un shared_ptr<>
aussi, non?Voici du code qui compile et, espérons-le, illustre les principaux points.
#include <iostream>
#include <memory>
struct Document {
std::string content;
};
struct UI {
std::shared_ptr<Document> doc;
// This function is not copying the object, but holding a
// reference to it to make sure it doesn't get destroyed.
void setDocument(std::shared_ptr<Document> newDoc) {
this->doc = newDoc;
}
void redraw() {
// do something with this->doc
}
};
// This function does not need to take a copy of the Document, so it
// should access it as efficiently as possible. At the moment it
// creates a whole new shared_ptr object which I feel is inefficient,
// but passing by reference does not work.
// It should also take a const parameter as it isn't modifying the
// object.
int charCount(std::shared_ptr<Document> doc)
{
// I realise this should be a member function inside Document, but
// this is for illustrative purposes.
return doc->content.length();
}
// This function is the same as charCount() but it does modify the
// object.
void appendText(std::shared_ptr<Document> doc)
{
doc->content.append("hello");
return;
}
// Create a derived type that the code above does not know about.
struct TextDocument: public Document {};
std::shared_ptr<TextDocument> createTextDocument()
{
return std::shared_ptr<TextDocument>(new TextDocument());
}
int main(void)
{
UI display;
// Use the factory function to create an instance. As a user of
// this class I don't want to have to worry about deleting the
// instance, but I don't really care what type it is, as long as
// it doesn't stop me from using it the way I need to.
auto doc = createTextDocument();
// Share the instance with the UI, which takes a copy of it for
// later use.
display.setDocument(doc);
// Use a free function which modifies the object.
appendText(doc);
// Use a free function which doesn't modify the object.
std::cout << "Your document has " << charCount(doc)
<< " characters.\n";
return 0;
}
Je vais répondre dans l'ordre inverse pour commencer par les cas simples.
Les fonctions utilitaires qui acceptent les objets des fonctions d'usine, les utilisent, mais n'en deviennent pas propriétaires. (Par exemple, une fonction qui compte le nombre de mots dans le document.)
Si vous appelez une fonction d'usine, vous vous appropriez toujours l'objet créé par la définition même d'une fonction d'usine. Je pense que ce que vous voulez dire, c'est que certains autres clients obtiennent d'abord un objet de l'usine et souhaitent ensuite le transmettre à la fonction d'utilité qui ne s'approprie pas lui-même.
Dans ce cas, la fonction d'utilité ne devrait pas se soucier du tout de la façon dont la propriété de l'objet sur lequel elle opère est gérée. Il doit simplement accepter une référence (probablement const
) ou - si "aucun objet" est une condition valide - un pointeur brut non propriétaire. Cela minimisera le couplage entre vos interfaces et rendra la fonction utilitaire plus flexible.
Fonctions qui gardent une référence à l'objet après leur retour (comme un composant d'interface utilisateur qui prend une copie de l'objet afin qu'il puisse dessiner le contenu à l'écran selon les besoins.)
Ceux-ci devraient prendre un std::shared_ptr
par valeur . Cela montre clairement à partir de la signature de la fonction qu'ils prennent la propriété partagée de l'argument.
Parfois, il peut également être utile d'avoir une fonction qui s'approprie de façon unique son argument (les constructeurs viennent à l'esprit). Ceux-ci devraient prendre un std::unique_ptr
par valeur (ou par référence rvalue) qui rendra également la sémantique claire de la signature.
Fonction d'usine (en dehors de la classe) qui crée des objets et les renvoie aux utilisateurs de la classe. (Par exemple, ouvrir un document et renvoyer un objet qui peut être utilisé pour accéder au contenu.)
C'est le plus difficile car il y a de bons arguments pour les deux, std::unique_ptr
et std::shared_ptr
. La seule chose claire est que le retour d'un pointeur brut propriétaire n'est pas bon.
Renvoyer un std::unique_ptr
est léger (pas de surcharge par rapport au retour d'un pointeur brut) et transmet la sémantique correcte d'une fonction d'usine. Celui qui a appelé la fonction obtient la propriété exclusive de l'objet fabriqué. Si nécessaire, le client peut construire un std::shared_ptr
sur un std::unique_ptr
au prix d'une allocation dynamique de mémoire.
En revanche, si le client a besoin d'un std::shared_ptr
de toute façon, il serait plus efficace d'utiliser l'usine std::make_shared
pour éviter l'allocation de mémoire dynamique supplémentaire. Il existe également des situations où vous devez simplement utiliser un std::shared_ptr
par exemple, si le destructeur de l'objet géré n'est pas_virtual
et que le pointeur intelligent doit être converti en pointeur intelligent en classe de base. Mais un std::shared_ptr
a plus de frais généraux qu'un std::unique_ptr
donc si cette dernière est suffisante, nous préférons éviter cela si possible.
Donc, en conclusion, je proposerais la ligne directrice suivante:
std::shared_ptr
.std::shared_ptr
de toute façon, utilisez le potentiel d'optimisation de std::make_shared
.std::unique_ptr
.Bien sûr, vous pouvez éviter le problème en fournissant deux fonctions d'usine, une qui renvoie un std::unique_ptr
et celui qui renvoie un std::shared_ptr
afin que chaque client puisse utiliser ce qui correspond le mieux à ses besoins. Si vous en avez besoin fréquemment, je suppose que vous pouvez supprimer la majeure partie de la redondance avec une méta-programmation de modèle intelligente.
Quel serait le meilleur type de retour pour la fonction d'usine?
unique_ptr
serait mieux. Il empêche les fuites accidentelles et l'utilisateur peut libérer la propriété du pointeur ou transférer la propriété vers un shared_ptr
(qui a un constructeur à cet effet), s'ils veulent utiliser un schéma de propriété différent.
Quel est le meilleur type de paramètre pour la fonction utilitaire?
Une référence, sauf si le flux du programme est si compliqué que l'objet peut être détruit pendant l'appel de fonction, auquel cas shared_ptr
ou weak_ptr
. (Dans les deux cas, il peut faire référence à une classe de base et ajouter des qualificatifs const
, si vous le souhaitez.)
Quel est le meilleur type de paramètre pour la fonction qui garde une référence à l'objet?
shared_ptr
ou unique_ptr
, si vous voulez qu'il assume la responsabilité de la durée de vie de l'objet et ne vous en souciez pas autrement. Un pointeur brut ou une référence, si vous pouvez (simplement et de manière fiable) arranger l'objet pour survivre à tout ce qui l'utilise.
La plupart des autres réponses couvrent cela, mais @ T.C. lié à quelques très bonnes lignes directrices que je voudrais résumer ici:
Fonction d'usine
Une fabrique qui produit un type de référence doit renvoyer un
unique_ptr
Par défaut, ou unshared_ptr
Si la propriété doit être partagée avec la fabrique. - GotW # 9
Comme d'autres l'ont souligné, vous, en tant que destinataire du unique_ptr
, Pouvez le convertir en shared_ptr
Si vous le souhaitez.
Paramètres de fonction
Ne transmettez pas un pointeur intelligent en tant que paramètre de fonction, sauf si vous souhaitez utiliser ou manipuler le pointeur intelligent lui-même, par exemple pour partager ou transférer la propriété. Préférez les objets de passage par valeur,
*
Ou&
, Pas par pointeur intelligent. - GotW # 91
En effet, lorsque vous passez par un pointeur intelligent, vous incrémentez le compteur de référence au début de la fonction et le décrémentez à la fin. Ce sont des opérations atomiques, qui nécessitent une synchronisation entre plusieurs threads/processeurs, donc dans le code fortement multithread, la pénalité de vitesse peut être assez élevée.
Lorsque vous êtes dans la fonction, l'objet ne va pas disparaître car l'appelant détient toujours une référence à lui (et ne peut rien faire avec l'objet jusqu'à ce que votre fonction revienne ) donc incrémenter le nombre de références est inutile si vous n'allez pas conserver une copie de l'objet après le retour de la fonction.
Pour les fonctions qui ne prennent pas possession de l'objet :
Utilisez un
*
Si vous devez exprimer null (pas d'objet), sinon préférez utiliser un&
; et si l'objet est en entrée uniquement, écrivezconst widget*
ouconst widget&
. - GotW # 91
Cela n'oblige pas votre appelant à utiliser un type de pointeur intelligent particulier - tout pointeur intelligent peut être converti en pointeur normal ou en référence. Donc, si votre fonction n'a pas besoin de conserver une copie de l'objet ou de s'en approprier, utilisez un pointeur brut. Comme ci-dessus, l'objet ne disparaîtra pas au milieu de votre fonction car l'appelant s'y accroche toujours (sauf dans des circonstances spéciales, dont vous seriez déjà au courant si cela vous pose un problème.)
Pour les fonctions qui prennent possession de l'objet :
Exprimez une fonction "puits" en utilisant un paramètre par valeur
unique_ptr
.
void f( unique_ptr<widget> );
Cela montre clairement que la fonction s'approprie l'objet, et il est possible de lui passer des pointeurs bruts que vous pourriez avoir à partir du code hérité.
Pour les fonctions qui prennent la propriété partagée de l'objet :
Exprimez qu'une fonction stockera et partagera la propriété d'un objet tas en utilisant un paramètre par valeur
shared_ptr
. - GotW # 91
Je pense que ces lignes directrices sont très utiles. Lisez les pages d'où proviennent les citations pour plus de contexte et d'explications approfondies, ça vaut le coup.
Je retournerais un unique_ptr
par valeur dans la plupart des situations. La plupart des ressources ne devraient pas être partagées, car cela rend difficile de raisonner sur leur durée de vie. Vous pouvez généralement écrire votre code de manière à éviter la propriété partagée. Dans tous les cas, vous pouvez faire un shared_ptr
du unique_ptr
, ce n'est donc pas comme si vous limitiez vos options.