Très bonne discussion sur le fil du Quizz C++ de l'été !
Sur le problème en lui-même, Clément V a donné la bonne réponse en parlant de temporaire dans le 4ème appel (shared_ptr de B). Je vais développer :
Les deux premiers appels sont identiques en terme de code machine. En effet, en C++, un objet B dérivant de A est, au point de vue de l'arrangement mémoire, un A, suivi des attributs de B (oublions l'héritage virtuel pour le moment). Donc, lorsque l'on passe un pointeur sur A ou un pointeur sur B à une fonction prenant un pointeur sur A, l'on n'a qu'à pousser le pointeur sur A ou B sur la pile et faire l'appel de fonction. La fonction n'accèdera qu'aux attributs de A, et tout le monde est content.
Le troisième appel n'a pas besoin de conversion, puisqu'il s'agit exactement du type passé, donc même code généré, on pousse le pointeur sur la pile et on fait l'appel.
Vient le cas du 4ème appel. En C++, il n'y a pas de conversion implicite entre des types templatés T<A>
et T<B>
. Donc, pour qu'un std::shared_ptr<B>
soit convertible en un std::shared_ptr<A>
et donc émuler au plus près un pointeur à poil, les concepteurs de la bibliothèque standard ont implémenté un constructeur qui permet cette conversion. Le voici dans toute sa gloire :
template< class Y >
shared_ptr( const shared_ptr<Y>& r ) noexcept;
Le compilateur va donc utiliser ce constructeur, qui va en interne tenter de convertir le type Y * en un type T *. Si c'est possible, alors le programme compile, mais l'on a effectivement fait une copie du pointeur partagé, avec toute la machinerie du comptage de références atomiques que cela implique. L'on peut d'ailleurs le voir très bien en implémentant la fonction g() de cette façon:
void g(const std::shared_ptr<A> & ptr)
{
std::cout << ptr.use_count() << std::endl;
}
Le troisième appel affichera 1, alors que le 4ème affichera 2.
Mon benchmark me donne les appels 1, 2 et 3 dans un mouchoir de poche, et l'appel à 4 qui est environ 2,5 fois plus lent.
Parlons maintenant style : de nombreux commentateurs ont affirmé, avec raison, qu'un passage de pointeur partagé par référence semblait être peu pertinent dans la grande majorité des cas, car soit l'appel va le copier, et donc il faudrait mieux passer par copie et déplacer, soit l'appel ne va pas le copier, et donc autant le passer en pointeur à poil pour contraindre l'interface le moins possible.
Sur le principe, je suis totalement d'accord.
Maintenant, dans la pratique, cela force à se poser beaucoup de questions sur chaque paramètre de chaque fonction, de changer les signatures si tout d'un coup on décide de copier le pointeur, et en plus c'est un peu casse-gueule car il faut bien faire attention de ne pas utiliser la variable après l'avoir déplacée, comme cela m'est arrivé plus qu'à mon tour.
Au turbin, c'est encore plus exacerbé par notre tendance à utiliser les shared_ptr un peu partout, comme des pointeurs à la Java. Nous avons donc décidé de garder la vieille convention: dans du code non critique, si c'est un type de base, on passe par copie, sinon, on passe systématiquement par référence constante. Après, dans du code critique, c'est une autre histoire.
# yep
Posté par fearan . Évalué à 4.
Chez nous c'est la même, je lutte tous les jours pour que les collègues arrêtent cette hérésie
Plus d'une fois on tombe sur du code
Le tout sans aucun contrôle sur l'existence de truc ( if (truc) par exemple )
Je parle même pas d'un quelconque const qu'on pourrait avoir
Attention hein je ne suis pas contre l'utilisation des shared_ptr, je suis contre leur utilisation systématique.
Il ne faut pas décorner les boeufs avant d'avoir semé le vent
Suivre le flux des commentaires
Note : les commentaires appartiennent à celles et ceux qui les ont postés. Nous n’en sommes pas responsables.