Cet article a fait l'objet d'une traduction automatique.

Windows et C++

Pointeurs intelligents COM revus

Kenny Kerr

Kenny KerrAprès la seconde venue du COM, autrement connu comme l'exécution de Windows, la nécessité d'un pointeur intelligent efficace et fiable pour les interfaces COM est plus importante que jamais. Mais ce qui en fait un bon pointeur intelligent de COM ? Le modèle de classe ATL CComPtr a été le pointeur intelligent de fait de COM pour ce qui ressemble à des décennies. Le Kit de développement logiciel (SDK) Windows pour Windows 8 introduit le modèle de classe ComPtr dans le cadre de la Windows Runtime C++ Template Library (WRL), dont certains saluée comme un substitut moderne pour le CComPtr ATL. Au début, je pensais aussi il s'agissait d'un bon pas en avant, mais après beaucoup d'expérience à l'aide de la ComPtr WRL, je suis venu à la conclusion, qu'elle doit être évitée. Pourquoi ? Continuez à lire.

Alors que faut-il faire ? Nous devrions revenir par ATL ? Absolument pas, mais peut-être il est temps d'appliquer certains le C++ modernes offerts par Visual C++ 2015 à la conception d'un nouveau pointeur intelligent pour les interfaces COM. Dans la fonction Connect() ; 2015 Visual Studio & Microsoft Azure numéro spécial, j'ai montré comment faire le plus de Visual C++ 2015 pour implémenter facilement IUnknown et IInspectable en utilisant le modèle de classe Implements. Maintenant je vais vous montrer comment utiliser plus de Visual C++ 2015 pour mettre en œuvre un nouveau modèle de classe ComPtr.

Pointeurs intelligents sont notoirement difficiles à écrire, mais grâce à C ++ 11, c'est pas presque aussi difficile qu'il était une fois. Partie de la raison à cela a à voir avec tous les trucs astucieux conçus pour contourner le manque d'expressivité dans le langage C++ et les bibliothèques standard les développeurs de bibliothèques, pour faire leurs propres objets agissent comme des pointeurs intégrés tout en restant efficace et correcte. En particulier, les références de rvalue vont un long chemin vers rend la vie tellement plus facile pour nous, les développeurs de bibliothèques. Une autre partie est simplement avec le recul — voir comment existant conçoit vos voies. Et, bien sûr, il y a dilemme du chaque développeur : faire preuve de modération et n'essaie ne pas d'emballer toutes les fonctionnalités imaginables dans une abstraction particulière.

Niveau le plus élémentaire, un pointeur intelligent COM doit fournir la gestion des ressources pour le pointeur d'interface COM sous-jacente. Cela implique que le pointeur intelligent sera un modèle de classe et stocker un pointeur d'interface du type souhaité. Techniquement, il n'a pas réellement besoin stocker un pointeur d'interface d'un type particulier, mais pourrait au contraire simplement stocker un pointeur d'interface IUnknown, mais ensuite le pointeur intelligent devra s'appuyer sur un static_cast, chaque fois que le pointeur intelligent est déréférencé. Cela peut être utile et dangereux sur le plan conceptuel, mais je vais en parler dans un prochain article. Pour l'instant, je vais commencer avec un modèle de classe de base pour stocker un pointeur fortement typé :

template <typename Interface>
class ComPtr
{
public:
  ComPtr() noexcept = default;
private:
  Interface * m_ptr = nullptr;
};

Longue date développeurs C++ peuvent se demander d'abord ce qu'il s'agit, mais les chances sont que les développeurs C++ plus actifs ne seront pas trop surpris. La variable de membre m_ptr s'appuie sur une grande nouvelle fonctionnalité qui permet aux membres de données statiques non être initialisé lorsqu'elles sont déclarées. Cela réduit considérablement le risque d'oublier accidentellement pour initialiser les variables membres que les constructeurs sont ajoutés et modifiés au fil du temps. Toute initialisation explicitement fournie par un constructeur particulier prévaut sur cette initialisation en place, mais la plupart du temps cela signifie que les constructeurs ne doivent pas s'inquiéter sur la définition des variables membres qui serait autrement ont commencé avec des valeurs imprévisibles.

Compte tenu de l'interface de pointeur est maintenant assuré pour être initialisé, je peux aussi compter sur une autre nouveauté à demander explicitement une définition par défaut des fonctions membres spéciaux. Dans l'exemple précédent, je demande la définition par défaut du constructeur par défaut — un constructeur par défaut par défaut, si vous voulez. Don' t shoot le Messager. Pourtant, la capacité par défaut ou supprimer des fonctions de membre spécial ainsi que la possibilité d'initialiser des variables de membre au moment de la déclaration sont parmi mes préférées fonctionnalités offertes par Visual C++ 2015. C'est les petites choses qui comptent.

Un pointeur intelligent COM doit offrir le service le plus important est celui de blindage le développeur contre les dangers de la modèle de décompte de références COM intrusif. En fait, j'aime l'approche de COM pour le comptage de référence, mais je veux une bibliothèque afin de prendre soin d'elle pour moi. Cette surface dans plusieurs endroits subtils à travers le modèle de classe ComPtr, mais est peut-être la plus évidente lorsqu'un appelant déréférence le pointeur intelligent. Je ne veux pas un appelant d'écrire quelque chose comme ce qui suit, accidentellement ou non :

ComPtr<IHen> hen;
hen->AddRef();

La possibilité d'appeler les fonctions virtuelles AddRef ou libération devrait être exclusivement sous l'égide du pointeur intelligent. Bien sûr, le pointeur intelligent doit permettre encore les autres méthodes à appeler via une opération de ce type déréférencement du. Normalement, les de déréférencement d'un pointeur intelligent opérateur pourrait ressembler à ceci :

Interface * operator->() const noexcept
{
  return m_ptr;
}

Cela fonctionne pour les pointeurs d'interface COM, et il n'y a pas besoin d'une affirmation parce qu'une violation d'accès est plus instructive. Mais cette implémentation permettra toujours à un appelant d'appeler AddRef et Release. La solution consiste simplement à retourner un type qui interdit AddRef et Release d'être appelée. Un petit modèle de la classe est très pratique :

template <typename Interface>
class RemoveAddRefRelease : public Interface
{
  ULONG __stdcall AddRef();
  ULONG __stdcall Release();
};

Le modèle de classe RemoveAddRefRelease hérite de toutes les méthodes de l'argument de modèle, mais déclare AddRef et Release privée afin qu'un appelant ne peut pas accidentellement renvoyer à ces méthodes. Le pointeur intelligent de déréférencer opérateur peut simplement utiliser static_cast pour protéger le pointeur d'interface retourné :

RemoveAddRefRelease<Interface> * operator->() const noexcept
{
  return static_cast<RemoveAddRefRelease<Interface> *>(m_ptr);
}

Il s'agit d'un exemple où mon ComPtr s'écarte de l'approche de la LFR. WRL opte pour faire tous privé de méthodes de IUnknown, y compris QueryInterface, et je ne vois aucune raison pour restreindre les appelants de cette façon. Cela veut dire que LFR remet inévitablement des solutions de rechange pour ce service essentiel et qui conduit à la complexité accrue et de la confusion pour les appelants.

Parce que mon ComPtr prend résolument le commandement du décompte de références, elle avait mieux le faire correctement. Eh bien, je vais commencer avec une paire de fonctions d'assistance privée commençant par un pour AddRef :

void InternalAddRef() const noexcept
{
  if (m_ptr)
  {
    m_ptr->AddRef();
  }
}

Ce n'est pas tout ce que passionnant, mais il existe une variété de fonctions qui requièrent une référence à prendre sous certaines conditions et cela assurera que j'ai fais la bonne chose à chaque fois. La fonction d'assistance correspondante pour diffusion immédiate est un peu plus subtile :

void InternalRelease() noexcept
{
  Interface * temp = m_ptr;
  if (temp)
  {
    m_ptr = nullptr;
    temp->Release();
  }
}

Pourquoi temporaire ? Eh bien, envisager la plus intuitive mais la mise en œuvre incorrecte qui reflète à peu près ce que j'ai fait (à juste titre) à l'intérieur de la fonction InternalAddRef :

if (m_ptr)
{
  m_ptr->Release(); // BUG!
  m_ptr = nullptr;
}

Le problème ici est que l'appel de la libération méthode pourrait déclencher une chaîne d'événements qui pourraient voir l'objet étant sorti une deuxième fois. Ce deuxième voyage à travers InternalRelease serait à nouveau trouver un pointeur d'interface non null et tenter de le libérer à nouveau. Il s'agit certes d'un scénario rare, mais le travail du développeur bibliothèque doit tenir compte de telles choses. L'implémentation originale impliquant un temporaire permet d'éviter cette double libération en tout d'abord détacher le pointeur d'interface du pointeur intelligent seulement puis libération. Regardant à travers les annales de l'histoire, il semble comme si Jim Springfield a été le premier à intercepter ce bogue épineux dans ATL. En tout cas, avec ces deux fonctions d'assistance à la main, je peux commencer à mettre en œuvre certaines des fonctions membres spéciaux qui aident à rendre l'objet obtenu agir et se sentir comme un objet incorporé. Le constructeur de copie est un exemple simple.

Contrairement aux pointeurs intelligents qui fournissent la propriété exclusive, construction de copie devrait être autorisée pour les pointeurs intelligents COM. Il faut éviter à tout prix les copies, mais si un appelant veut vraiment une copie ensuite une copie est ce qu'il obtient. Voici un constructeur de copie simple :

ComPtr(ComPtr const & other) noexcept :
  m_ptr(other.m_ptr)
{
  InternalAddRef();
}

Il s'occupe de l'exemple flagrant de la construction de copie. Il copie le pointeur d'interface avant d'appeler à l'aide de InternalAddRef. Si j'ai quitté il ya, copiant un ComPtr me sentirais principalement comme un pointeur intégré, mais pas tout à fait ainsi. Je pourrais, par exemple, créez une copie comme ceci :

ComPtr<IHen> hen;
ComPtr<IHen> another = hen;

Cela reflète ce que je peux faire avec pointeurs bruts :

IHen * hen = nullptr;
IHen * another = hen;

Mais les pointeurs crus cela possible aussi :

IUnknown * unknown = hen;

Avec mon constructeur de copie simple, je ne suis pas autorisé à faire la même chose avec ComPtr :

ComPtr<IUnknown> unknown = hen;

Même si inconnue doit finalement dériver de IUnknown, ComPtr<problente> ne dérivent de ComPtr<IUnknown> et le compilateur considère les types sans rapport avec eux. Ce que j'ai besoin est un constructeur qui agit comme un constructeur de copie logique pour d'autres objets ComPtr liées de manière logique — plus précisément, toute ComPtr avec un argument de modèle convertible en argument de modèle de le ComPtr construit. Ici, WRL s'appuie sur les traits de type, mais ce n'est pas réellement nécessaire. Tout ce que j'ai besoin est un modèle de fonction de prévoir la possibilité de conversion et puis je vais simplement laisser le compilateur vérifier si elle est effectivement convertible :

template <typename T>
ComPtr(ComPtr<T> const & other) noexcept :
  m_ptr(other.m_ptr)
{
  InternalAddRef();
}

C'est quand l'autre pointeur est utilisé pour initialiser le pointeur d'interface de l'objet que le compilateur vérifie si la copie est réellement significative. Donc ce sera compilé :

ComPtr<IHen> hen;
ComPtr<IUnknown> unknown = hen;

Mais cela ne sera pas :

ComPtr<IUnknown> unknown;
ComPtr<IHen> hen = unknown;

Et c'est comme il se doit. Bien sûr, le compilateur considère toujours les deux tout à fait différents types, ainsi le modèle du constructeur n'aurez réellement accès à la variable membre privée de l'autre, à moins que je leur fais des amis :

template <typename T>
friend class ComPtr;

Vous pourriez être tenté d'enlever une partie du code redondant parce qu'inconnue est convertible en inconnue. Pourquoi ne pas simplement supprimer le constructeur de copie réelle ? Le problème est que cet deuxième constructeur n'est pas considéré comme un constructeur de copie par le compilateur. Si vous omettez le constructeur de copie, le compilateur suppose que vous vouliez supprimer de l'objet à toute référence à cette fonction supprimée. Onward.

Avec la construction de copie prise en charge, il est très important que ComPtr fournissent également construction de déménagement. Si un déplacement est autorisé dans un scénario donné, ComPtr devrait permettre au compilateur d'opter pour qui il enregistrera une bosse de référence, qui est beaucoup plus coûteuse en comparaison à une opération de déplacement. Un constructeur de déplacement est encore plus simple que le constructeur de copie, parce qu'il n'y a pas besoin d'appeler InternalAddRef :

ComPtr(ComPtr && other) noexcept :
  m_ptr(other.m_ptr)
{
  other.m_ptr = nullptr;
}

Il copie le pointeur d'interface avant de compensation ou de réinitialiser le pointeur dans la référence rvalue, ou l'objet déplacé de. Dans ce cas, toutefois, le compilateur n'est pas si pointilleux, et vous pouvez simplement évitent ce constructeur de déplacement pour une version générique qui prend en charge les types convertibles :

template <typename T>
ComPtr(ComPtr<T> && other) noexcept :
  m_ptr(other.m_ptr)
{
  other.m_ptr = nullptr;
}

Et qui se termine les constructeurs ComPtr. Le destructeur est prévisible simple :

~ComPtr() noexcept
{
  InternalRelease();
}

J'ai déjà prise en compte des nuances de destruction à l'intérieur du programme d'assistance InternalRelease, donc ici, je peux réutiliser tout simplement que la bonté. J'ai discuté copie et déplacer construction, mais les opérateurs d'assignation correspondant doivent également être fournis pour ce pointeur intelligent pour se sentir comme un véritable pointeur. Afin de prendre en charge ces opérations, je vais ajouter une autre paire de fonctions d'assistance privée. Le premier est d'acquérir en toute sécurité une copie d'un pointeur d'interface donnée :

void InternalCopy(Interface * other) noexcept
{
  if (m_ptr != other)
  {
    InternalRelease();
    m_ptr = other;
    InternalAddRef();
  }
}

En supposant que les pointeurs d'interface ne sont pas égales (ou pas les deux pointeurs null), la fonction libère toute référence existante avant de prendre une copie du pointeur et obtenir une référence au pointeur d'interface nouvelle. De cette façon, je peux facilement appeler InternalCopy pour s'approprier d'une référence unique pour l'interface donnée, même si le pointeur intelligent conserve déjà une référence. De même, le deuxième programme d'assistance traite de déplacer en toute sécurité un pointeur d'interface donnée, ainsi que le décompte de références qu'il représente :

template <typename T>
void InternalMove(ComPtr<T> & other) noexcept
{
  if (m_ptr != other.m_ptr)
  {
    InternalRelease();
    m_ptr = other.m_ptr;
    other.m_ptr = nullptr;
  }
}

Alors que InternalCopy naturellement prend en charge que les types de convertibles, cette fonction est un modèle pour fournir cette fonctionnalité pour le modèle de classe. Sinon, InternalMove est en grande partie les mêmes, mais logiquement se déplace le pointeur d'interface, plutôt que d'acquérir une référence supplémentaire. Avec celui de la route, je peux implémenter les opérateurs d'assignation tout simplement. Tout d'abord, l'assignation de copie, et comme avec le constructeur de copie, je dois fournir la forme canonique :

ComPtr & operator=(ComPtr const & other) noexcept
{
  InternalCopy(other.m_ptr);
  return *this;
}

Je peux ensuite fournir un modèle pour les types de convertibles :

template <typename T>
ComPtr & operator=(ComPtr<T> const & other) noexcept
{
  InternalCopy(other.m_ptr);
  return *this;
}

Mais comme le constructeur de déménagement, je peux simplement fournir une seule version générique de la cession de déménagement :

template <typename T>
ComPtr & operator=(ComPtr<T> && other) noexcept
{
  InternalMove(other);
  return *this;
}

Tandis que la sémantique de déplacement est souvent supérieure à copier quand il s'agit de pointeurs intelligents contenant des références, mouvements ne sont pas sans coût, et une excellente façon d'éviter les déplacements dans certains scénarios clés est de fournir la sémantique de swap. Plusieurs types de conteneurs privilégiera les opérations de swap aux déménagements, ce qui peuvent éviter la construction d'une énorme charge d'objets temporaires. Mise en œuvre de la fonctionnalité swap pour ComPtr est assez simple :

void Swap(ComPtr & other) noexcept
{
  Interface * temp = m_ptr;
  m_ptr = other.m_ptr;
  other.m_ptr = temp;
}

Je voudrais utiliser l'algorithme d'échange Standard, mais, au moins dans l'implémentation de Visual C++, la nécessaire <utilitaire> en-tête inclut également indirectement <stdio.h> et je ne veux vraiment pas de forcer les développeurs à y compris tout cela juste pour le swap. Bien sûr, pour les algorithmes génériques trouver ma méthode Swap, j'ai besoin de fournir également une fonction de tiers swap (en minuscules) :

template <typename Interface>
void swap(ComPtr<Interface> & left, 
  ComPtr<Interface> & right) noexcept
{
  left.Swap(right);
}

Tant que cela est défini dans le même espace de noms comme le modèle de classe ComPtr, le compilateur permettra heureusement des algorithmes génériques rendre l'utilisation de l'approche sectorielle.

Une autre fonctionnalité intéressante de C ++ 11 est celui des opérateurs de conversion explicite. Historiquement, il a fallu quelques hacks malpropres pour produire un opérateur booléen fiable et explicit pour vérifier si un pointeur intelligent n'était logiquement pas null. Aujourd'hui, c'est aussi simple que cela :

explicit operator bool() const noexcept
{
  return nullptr != m_ptr;
}

Et qui s'occupe de l'extraordinaire et membres pratiquement spéciaux qui font mon pointeur intelligent comportent un peu comme un type intégré avec toute l'aide que je peux éventuellement fournir pour optimiser le compilateur toute surcharge de suite. Ce qui reste est une petite sélection d'assistants qui sont nécessaires pour les applications COM dans de nombreux cas. C'est où il faut pour éviter d'ajouter trop de cloches et de sifflets. Pourtant, il y a une poignée de fonctions sur lequel reposera presque n'importe quelle application non triviaux ou du composant. Tout d'abord, il a besoin d'un moyen de libérer explicitement la référence sous-jacente. C'est assez facile :

void Reset() noexcept
{
  InternalRelease();
}

Et puis il a besoin d'obtenir le pointeur sous-jacente, l'appelant ne devrait passer en tant qu'argument à une autre fonction :

Interface * Get() const noexcept
{
  return m_ptr;
}

Je pourrais avoir besoin de détacher la référence, peut-être pour renvoyer à un appelant :

Interface * Detach() noexcept
{
  Interface * temp = m_ptr;
  m_ptr = nullptr;
  return temp;
}

Je pourrais avoir besoin de faire une copie d'un pointeur existant. Cela pourrait être une référence détenue par l'appelant que j'aimerais tenir fermement :

void Copy(Interface * other) noexcept
{
  InternalCopy(other);
}

Ou je pourrais avoir un pointeur brut qui possède une référence à sa cible que je voudrais joindre sans une référence supplémentaire à fournir. Cela peut aussi être utile pour les références de coalescents dans de rares cas :

void Attach(Interface * other) noexcept
{
  InternalRelease();
  m_ptr = other;
}

Les fonctions quelques finales jouent un rôle particulièrement crucial, donc je vais passer quelques instants de plus sur eux. Méthodes COM retournent traditionnellement références comme des paramètres out via un pointeur vers un pointeur. Il est important que n'importe quel pointeur intelligent COM offre un moyen de capturer directement ces références. Pour cela j'ai fournit la méthode GetAddressOf :

Interface ** GetAddressOf() noexcept
{
  ASSERT(m_ptr == nullptr);
  return &m_ptr;
}

Il s'agit encore une fois où mon ComPtr écarte l'application de la LFR de manière subtile mais très critique. Notez que GetAddressOf affirme qu'il ne tient pas une référence avant de retourner son adresse. C'est vital, sinon la fonction appelée écrasera simplement quelque référence ont eu lieu et vous avez vous-même une fuite de référence. Sans l'affirmation, ces bugs sont beaucoup plus difficiles à détecter. Sur l'autre extrémité du spectre est la capacité à donner des références, soit du même type ou d'autres interfaces de que l'objet sous-jacent peut implémenter. Si vous souhaitez une autre référence à la même interface, je peux éviter d'appeler QueryInterface et simplement retourner une référence supplémentaire à l'aide de la convention prescrite par COM :

void CopyTo(Interface ** other) const noexcept
{
  InternalAddRef();
  *other = m_ptr;
}

Et vous pouvez l'utiliser comme suit :

hen.CopyTo(copy.GetAddressOf());

Dans le cas contraire, QueryInterface lui-même peut être employée avec aucune autre aide de ComPtr :

HRESULT hr = hen->QueryInterface(other.GetAddressOf());

En fait, cela s'appuie sur un modèle de fonction fourni directement par IUnknown pour éviter de devoir fournir explicitement le GUID de l'interface.

Enfin, il y a souvent des cas où une application ou un composant doit interroger une interface sans nécessairement passer à l'appelant dans la convention de COM classique. Dans ces cas, il est plus logique pour retourner ce nouveau pointeur d'interface niché confortablement à l'intérieur d'un autre ComPtr, comme suit :

template <typename T>
ComPtr<T> As() const noexcept
{
  ComPtr<T> temp;
  m_ptr->QueryInterface(temp.GetAddressOf());
  return temp;
}

Je peux utiliser puis simplement le bool operator explicite pour vérifier si la requête a réussi. Enfin, ComPtr fournit également tous les opérateurs de comparaison de tiers prévue par commodité et pour soutenir les divers des conteneurs et des algorithmes génériques. Encore une fois, ceci juste contribue à faire du pointeur intelligent agir et de se sentir plus comme un pointeur intégré, tout en fournissant les services essentiels pour bien gérer les ressources et fournir les services nécessaires qu'applications COM et composants s'attendre. Le modèle de classe ComPtr est juste un autre exemple de C++ moderne pour l'exécution de Windows (moderncpp.com).


Kenny Kerr est un programmeur informatique basé au Canada, mais aussi un auteur pour Pluralsight et MVP Microsoft. Il blogs à kennykerr.ca et vous pouvez le suivre sur Twitter à twitter.com/kennykerr.

Grâce à l'expert technique Microsoft suivant d'avoir relu cet article : James McNellis