Eh oui, pourquoi l’implémentation de std::shared_ptr est meilleure dans Visual C++ 2012 que dans les autres compilateurs ?
“Parce qu’elle utilise moins de mémoire, et qu’elle est plus rapide”.
Cette réponse est à l’image de la librairie STL : concise, efficace, mystérieuse ;).C’est aussi un prétexte à creuser le fonctionnement interne de shared_ptr. Allons-y.
Comment fonctionne un shared_ptr ?
En fait, un shared_ptr est un peu plus qu’un pointeur et un compteur de références. Comme la plupart du code des STL, shared_ptr est un code très dense, et… intelligent !
Un shared_ptr est un objet contenant deux pointeurs, soit 8 ou 16 octets selon qu’on compile en 32 ou 64 bits.
Voici un tout petit extrait de la définition de la classe _Ptr_base, qui sert de base pour shared_ptr et weak_ptr:
template<class _Ty>
class _Ptr_base
{ // base class for shared_ptr and weak_ptr
// ....
private:
_Ty *_Ptr;
_Ref_count_base *_Rep;
};
On y repère _Ptr, le pointeur vers l’objet lui-même, et _Rep, le pointeur vers un objet de type _Ref_count_base.
L’objet _Ref_count_base contient les compteurs de référence :
template<class _Ty>
class _Ref_count_base
{
private:
long _Uses;
long _Weaks;
_Ty * _Ptr;
// ...
};
Cette classe contient deux compteurs de références, en plus du pointeur vers l’objet lui-même :
- un compteur de références pour les shared_ptr (strong ref)
- un compteur de références pour les weak_ptr qui pointent vers l’objet (weak ref)
La création d’un shared_ptr provoque la création de deux objets :
- l’objet shared_ptr lui même (dérivant de _Ptr_base), qui contient deux pointeurs
- un objet de type _Ref_count_base, qui contient les compteurs de références.
Et c’est sans compter l’objet sur lequel pointe le shared_ptr ! Cela fait trois objets en tout, pour un seul shared_ptr !

Trois objets pour avoir un seul objet et un shared_ptr. C’est ainsi que fonctionnent les shared_ptr dans boost, gcc, clang, et… Visual C++ 2010.
Evidemment, si l’on crée un deuxième shared_ptr sur le même objet, seul un nouvel objet de type _Ptr_base sera créé. Le même objet _Ref_count_base sera utilisé pour les deux shared_ptr :

Accessoirement, on peut se demander pourquoi l’on a besoin de deux objets supplémentaires. Pourquoi est-ce que le shared_ptr n’est pas simplement l’adresse de l’objet _Ref_count_base ? Pourquoi a-t’on besoin d’un objet _Ptr_base en plus d’un _Ref_count_base ? Et pourquoi le pointeur vers l’objet cible (_Ty *_Ptr) est-il dupliqué dans _Ref_count_base et dans _Ptr_base ? Eh bien tout simplement pour que l’utilisation d’un shared_ptr soit aussi rapide qu’un pointeur classique. Pour que le déréférencement d’un shared_ptr soit rapide. Le fait d’avoir un pointeur vers l’objet cible (_Ty *_Ptr) en premier dans le shared_ptr permet d’accéder à l’objet cible plus rapidement, sans utiliser d’offset.
L’optimisation de shared_ptr dans Visual C++ 2012
Mais dans Visual C++ 2012 (VS11), Microsoft a été plus malin, et a remplacé le pointeur vers l’objet cible (_Ty *Ptr) par un champ _Storage bien mystérieux :
class _Ref_count_base
{
private:
long _Uses;
long _Weaks;
typename aligned_storage<sizeof (_Ty),
alignment_of<_Ty>::value>::type _Storage;
// ...
};
Le champ _Storage correspond à l’objet alloué. _Storage est initialisé par make_shared, à la taille de l’objet pointé par notre shared_ptr (sizeof(_Ty)). Ainsi, l’objet cible du shared_ptr est placé directement dans l’objet _Ref_count_base.

L’avantage est que cela représente une indirection, une allocation, et un pointeur (4 ou 8 octets) de moins. C’est à la fois peu et beaucoup.
C’est peu car cela ne représente que 4 ou 8 octets. Mais c’est énorme car un programme peut contenir des milliers de shared_ptr. La création d’un shared_ptr devient plus rapide avec make_shared, et consomme moins de mémoire. Multiplié par le nombre de shared_ptr dans une application, cela peut faire beaucoup !
Gageons que cette optimisation sera reprise par Boost et les autres compilateurs. C’est le souhait qu’a exprimé Stephan T. Lavavej, programmeurs des librairies STL dans l’équipe Visual C++, lors de la conférence Microsoft Going Native 2012 sur le langage C++.
(
Entre parenthèse, une petite astuce.
Lorsqu’on passe un shared_ptr à une fonction, il est recommandé d’utiliser un passage par référence const plutôt qu’un passage par valeur :
void fonc1(const shared_ptr<type> &pointeur);
void fonc2(shared_ptr<type> pointeur);
Le passage par valeur (fonc2) provoquera la création d’un nouvel objet shared_ptr et son initialisation par son move-constructor. Le passage par référence (fonc1) est plus efficace.
)