Passer shared_ptr <Derived> comme shared_ptr <Base>

93

Quelle est la meilleure méthode pour passer un shared_ptrd'un type dérivé à une fonction qui prend un shared_ptrd'un type de base?

Je passe généralement shared_ptrs par référence pour éviter une copie inutile:

int foo(const shared_ptr<bar>& ptr);

mais cela ne fonctionne pas si j'essaye de faire quelque chose comme

int foo(const shared_ptr<Base>& ptr);

...

shared_ptr<Derived> bar = make_shared<Derived>();
foo(bar);

je pourrais utiliser

foo(dynamic_pointer_cast<Base, Derived>(bar));

mais cela semble sous-optimal pour deux raisons:

  • A dynamic_castsemble un peu excessif pour une simple distribution dérivée à la base.
  • Si je comprends bien, dynamic_pointer_castcrée une copie (quoique temporaire) du pointeur à passer à la fonction.

Y a-t-il une meilleure solution?

Mise à jour pour la postérité:

Il s'est avéré être un problème de fichier d'en-tête manquant. De plus, ce que j'essayais de faire ici est considéré comme un anti-modèle. Généralement,

  • Les fonctions qui n'ont pas d'impact sur la durée de vie d'un objet (c'est-à-dire que l'objet reste valide pendant toute la durée de la fonction) doivent prendre une simple référence ou un pointeur, par exemple int foo(bar& b).

  • Les fonctions qui consomment un objet (c'est-à-dire sont les utilisateurs finaux d'un objet donné) devraient prendre une unique_ptrvaleur par, par exemple int foo(unique_ptr<bar> b). Les appelants doivent std::movela valeur dans la fonction.

  • Les fonctions qui prolongent la durée de vie d'un objet doivent prendre une shared_ptrvaleur par, par exemple int foo(shared_ptr<bar> b). Les conseils habituels pour éviter les références circulaires s'appliquent.

Voir la présentation de Herb Sutter Back to Basics pour plus de détails.

Matt Kline
la source
8
Pourquoi voulez-vous passer un shared_ptr? Pourquoi pas de référence const de barre?
ipc
2
Tout dynamiccasting n'est nécessaire que pour le downcasting. En outre, passer le pointeur dérivé devrait fonctionner correctement. Il va créer un nouveau shared_ptravec le même refcount (et l'augmenter) et un pointeur vers la base, qui se lie ensuite à la référence const. Puisque vous prenez déjà une référence, cependant, je ne vois pas pourquoi vous voulez en prendre un shared_ptr. Prenez un Base const&et appelez foo(*bar).
Xeo
@Xeo: Passer le pointeur dérivé (ie foo(bar)) ne fonctionne pas, du moins dans MSVC 2010.
Matt Kline
1
Qu'entendez-vous par «manifestement ne fonctionne pas»? Le code se compile et se comporte correctement; demandez-vous comment éviter de créer un temporaire shared_ptrà passer à la fonction? Je suis assez sûr qu'il n'y a aucun moyen d'éviter cela.
Mike Seymour
1
@Seth: Je ne suis pas d'accord. Je pense qu'il y a des raisons de passer un pointeur partagé par valeur, et il y a très peu de raisons de passer un pointeur partagé par référence (et tout cela sans préconiser des copies inutiles). Raisonnement ici stackoverflow.com/questions/10826541/…
R. Martinho Fernandes

Réponses:

47

Bien que Baseet Derivedsoient covariants et bruts, les pointeurs agissent en conséquence shared_ptr<Base>et neshared_ptr<Derived> sont pas covariants. Il dynamic_pointer_casts'agit de la manière correcte et la plus simple de gérer ce problème.

( Modifier: static_pointer_cast serait plus approprié car vous effectuez un cast de dérivé vers la base, ce qui est sûr et ne nécessite pas de vérifications à l'exécution. Voir les commentaires ci-dessous.)

Cependant, si votre foo()fonction ne souhaite pas participer à l'extension de la durée de vie (ou, plutôt, participer à la propriété partagée de l'objet), il est préférable d'accepter a const Base&et de déréférencer le shared_ptrlors de sa transmission foo().

void foo(const Base& base);
[...]
shared_ptr<Derived> spDerived = getDerived();
foo(*spDerived);

En passant, comme les shared_ptrtypes ne peuvent pas être covariants, les règles des conversions implicites entre les types de retour covariants ne s'appliquent pas lors du retour des types de shared_ptr<T>.

Bret Kuhns
la source
39
Ils ne sont pas covariants, mais shared_ptr<Derived>sont implicitement convertibles en shared_ptr<Base>, donc le code devrait fonctionner sans manigances de casting.
Mike Seymour
9
Um, shared_ptr<Ty>a un constructeur qui prend a shared_ptr<Other>et effectue la conversion appropriée si Ty*est implicitement convertible en Other*. Et si un casting est nécessaire, celui-ci static_pointer_castest approprié ici, non dynamic_pointer_cast.
Pete Becker
Vrai, mais pas avec son paramètre de référence, comme dans la question. Il aurait besoin d'en faire une copie, peu importe. Mais, s'il utilise refs to shared_ptrpour éviter le nombre de références, alors il n'y a vraiment aucune bonne raison d'utiliser a shared_ptren premier lieu. Il est préférable d'utiliser à la const Base&place.
Bret Kuhns
@PeteBecker Voir mon commentaire à Mike sur le constructeur de conversion. Honnêtement, je n'en savais rien static_pointer_cast, merci.
Bret Kuhns
1
@TanveerBadar Pas sûr. Peut-être que cela n'a pas été compilé en 2012? (en utilisant spécifiquement Visual Studio 2010 ou 2012). Mais vous avez tout à fait raison, le code d'OP devrait absolument être compilé si la définition complète d'une classe / publiquement dérivée / est visible par le compilateur.
Bret Kuhns
32

Cela se produira également si vous avez oublié de spécifier l' héritage public sur la classe dérivée, c'est à dire si comme moi vous écrivez ceci:

class Derived : Base
{
};
dshepherd
la source
classest pour les paramètres de modèle; structsert à définir les classes. (C'est au plus 45% une blague.)
Davis Herring
Cela devrait certainement être considéré comme la solution, il n'y a pas besoin de casting car il ne manque que du public.
Alexis Paques
12

On dirait que vous essayez trop fort. shared_ptrest bon marché à copier; c'est l'un de ses objectifs. Les faire circuler par référence ne fait pas grand-chose. Si vous ne voulez pas de partage, passez le pointeur brut.

Cela dit, il y a deux façons de faire cela auxquelles je peux penser du haut de ma tête:

foo(shared_ptr<Base>(bar));
foo(static_pointer_cast<Base>(bar));
Pete Becker
la source
9
Non, ils ne sont pas bon marché à copier, ils doivent être passés par référence dans la mesure du possible.
Seth Carnegie
6
@SethCarnegie - Herb a-t-il profilé votre code pour voir si le passage par valeur était un goulot d'étranglement?
Pete Becker
25
@SethCarnegie - cela ne répond pas à la question que j'ai posée. Et, pour ce que ça vaut, j'ai écrit l' shared_ptrimplémentation fournie par Microsoft.
Pete Becker
6
@SethCarnegie - vous avez l'heuristique à l'envers. Les optimisations manuelles ne doivent généralement pas être effectuées à moins que vous ne puissiez montrer qu'elles sont nécessaires.
Pete Becker
21
Ce n'est qu'une optimisation "prématurée" si vous avez besoin d'y travailler. Je ne vois aucun problème à adopter des idiomes efficaces plutôt que des idiomes inefficaces, que cela fasse une différence dans un contexte particulier ou non.
Mark Ransom
11

Vérifiez également que le #includefichier d'en-tête contenant la déclaration complète de la classe dérivée se trouve dans votre fichier source.

J'ai eu ce problème. Le std::shared<derived>ne serait pas jeté std::shared<base>. J'avais déclaré en avant les deux classes afin que je puisse contenir des pointeurs vers elles, mais parce que je n'avais pas #includele compilateur ne pouvait pas voir qu'une classe était dérivée de l'autre.

Phil Rosenberg
la source
1
Wow, je ne m'y attendais pas mais cela l'a corrigé pour moi. Je faisais très attention et n'incluais que les fichiers d'en-tête là où j'en avais besoin, donc certains d'entre eux n'étaient que dans les fichiers source et les déclaraient dans les en-têtes comme vous l'avez dit.
jigglypuff
Un compilateur stupide est stupide. C'était mon problème. Merci!
Tanveer Badar le