Partager via


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

Windows et C++

Visual C++ 2015 apporte Legacy Code C++ moderne

Kenny Kerr

Kenny KerrSystèmes de programmation avec Windows dépendent fortement de handles opaques qui représentent des objets cachés derrière des API de type C. Sauf si vous êtes de programmation à un niveau assez élevé, les chances sont que vous serez dans le secteur de la gestion des poignées de toutes sortes. Le concept d'un handle existe dans beaucoup de bibliothèques et de plates-formes et n'est certes pas unique au système d'exploitation Windows. J'ai d'abord écrit sur un modèle de classe de poignée intelligente en 2011 (msdn.microsoft.com/magazine/hh288076) démarrage de Visual C++ présentant certaines fonctionnalités de 11 langage C ++ initial. Visual C++ 2010 a permis d'écrire la poignée pratique et sémantiquement correcte des wrappers, mais son appui pour C ++ 11 était minime et beaucoup d'efforts était encore nécessaire d'écrire une telle classe correctement. Avec l'introduction de Visual C++ 2015 cette année, j'ai pensé revenir sur ce sujet et partager encore plus d'idées sur l'utilisation de C++ modernes pour agrémenter certaines anciennes bibliothèques de style C.

Les bonnes librairies n'allouer toutes les ressources et, ainsi, exiger des emballage minimal. Mon exemple préféré est le verrou de Windows (SRW) slim reader/writer. Voici tout ce qu'il faut pour créer et initialiser un verrou SRW qui est prêt à l'emploi :

SRWLOCK lock = {};

La structure de verrou SRW contient juste un seul vide * pointeur et il n'y a rien à nettoyer ! Il doit être initialisé avant de l'utiliser et la seule restriction est qu'il ne peut pas être déplacé ou copié. Évidemment, toute modernisation a plus à voir avec la sécurité d'exception, tandis que le verrou est maintenu, plutôt que de la gestion des ressources. Pourtant, C++ modernes peuvent aider à assurer que ces exigences simples. Tout d'abord, je peux utiliser la capacité pour initialiser les membres non statiques de données lorsqu'elles sont déclarées pour préparer le verrou SRW pour utilisation :

class Lock
{
  SRWLOCK m_lock = {};
};

Qui prend en charge l'initialisation, mais encore, le verrou peut être copié et déplacé. Pour cela, je dois supprimer le constructeur de copie par défaut et opérateur d'assignation de copie :

class Lock
{
  SRWLOCK m_lock = {};
public:
  Lock(Lock const &) = delete;
  Lock & operator=(Lock const &) = delete;
};

Vous éviterez les copies et déplacements. Dans la partie publique de la classe déclarant tend à produire les meilleurs messages d'erreur du compilateur. Bien sûr, maintenant je dois fournir un constructeur par défaut, parce qu'une personne est supposée n'est plus :

class Lock
{
  SRWLOCK m_lock = {};
public:
  Lock() noexcept = default;
  Lock(Lock const &) = delete;
  Lock & operator=(Lock const &) = delete;
};

Malgré le fait que je n'ai pas écrit tout le code, en soi, le compilateur génère tout pour moi — je peux maintenant créer un verrou tout simplement :

Lock lock;

Le compilateur va interdire toute tentative de copier ou déplacer le verrou :

Lock lock2 = lock; // Error: no copy!
Lock lock3 = std::move(lock); // Error: no move!

Ensuite, j'ajouterai simplement les méthodes d'acquisition et libéré le verrou de diverses manières. Le verrou SRW, comme son nom l'indique, assure lecteur partagé et sémantique de verrouillage exclusif écrivain. Figure 1 fournit un ensemble de méthodes pour un verrouillage exclusif simple minimal.

Verrou SRW Simple et efficace la figure 1

class Lock
{
  SRWLOCK m_lock = {};
public:
  Lock() noexcept = default;
  Lock(Lock const &) = delete;
  Lock & operator=(Lock const &) = delete;
  void Enter() noexcept
  {
    AcquireSRWLockExclusive(&m_lock);
  }
  void Exit() noexcept
  {
    ReleaseSRWLockExclusive(&m_lock);
  }
};

Découvrez « The Evolution of synchronisation dans Windows et C++ » (msdn.microsoft.com/magazine/jj721588) pour plus d'informations sur la magie derrière cette incroyable verrouillage peu primitive. Tout ce qui reste est de fournir un peu de sécurité d'exception autour de la propriété verrouillage. Je ne voudrais certainement pas écrire quelque chose comme ceci :

lock.Enter();
// Protected code
lock.Exit();

Je préférerais un blocage de carter pour s'occuper d'acquisition et libéré le verrou pour une portée donnée :

Lock lock;
{
  LockGuard guard(lock);
  // Protected code
}

Tel un gardien de la serrure peut simplement conserver une référence à la sous­serrure couché :

class LockGuard
{
  Lock & m_lock;
};

Comme la classe de verrou lui-même, il est préférable que la classe garde ne permet pas des copies ou des mouvements, soit :

class LockGuard
{
  Lock & m_lock;
public:
  LockGuard(LockGuard const &) = delete;
  LockGuard & operator=(LockGuard const &) = delete;
};

Reste plus qu'un constructeur d'entrer le verrou et le destructeur à sortir de l'écluse. La figure 2 cet exemple se termine.

Figure 2 serrure Simple garde

class LockGuard
{
  Lock & m_lock;
public:
  LockGuard(LockGuard const &) = delete;
  LockGuard & operator=(LockGuard const &) = delete;
  explicit LockGuard(Lock & lock) noexcept :
    m_lock(lock)
  {
    m_lock.Enter();
  }
  ~LockGuard() noexcept
  {
    m_lock.Exit();
  }
};

Pour être juste, le verrou SRW Windows est un petit bijou unique et la plupart des bibliothèques nécessitera un peu de stockage ou un type de ressource qui doit être gérée de manière explicite. Je vous ai déjà montré la meilleure façon de gérer les pointeurs d'interface COM dans « COM Smart Pointers Revisited » (msdn.microsoft.com/magazine/dn904668), alors maintenant je vais me concentrer sur le cas plus général de handles opaques. Comme je l'écrivais previ­ously, un modèle de classe du handle doit fournir un moyen de paramétrer non seulement le type de la poignée, mais aussi la façon dont le descripteur est fermé, et même ce que représente exactement un handle non valide. Pas toutes les bibliothèques utilisent une valeur null ou zéro valeur pour représenter des handles non valides. Mon modèle de classe poignée originale a assumé que l'appelant fournirait une classe de traits de poignée qui donne les informations nécessaires de la sémantique et le type. Après avoir écrit les classes de caractères beaucoup, beaucoup dans l'intervalle, je suis venu à réaliser que la grande majorité d'entre eux suive un modèle similaire. Et, comme n'importe quel développeur C++ vous le diront, les patrons sont quels modèles sont aptes à décrire. Ainsi, avec un modèle de classe de poignée, j'emploie maintenant un modèle de classe de caractères de poignée. Le modèle de classe de caractères handle n'est pas obligatoire, mais simplifie la plupart des définitions. Voici sa définition :

template <typename T>
struct HandleTraits
{
  using Type = T;
  static Type Invalid() noexcept
  {
    return nullptr;
  }
  // Static void Close(Type value) noexcept;
};

Remarquez ce que fournit le modèle de classe HandleTraits et ce qui précisément ne fournit pas. J'ai écrit tellement de méthodes non valides qui a retourné des valeurs nullptr que cela semblait être un défaut évident. En revanche, chaque classe de traits concrets doit fournir sa propre méthode Close pour des raisons évidentes. Le commentaire se nourrit simplement comme un modèle à suivre. L'alias de type est également facultatif et est simplement une commodité pour définir mes propres classes de caractères dérivée de ce modèle. Donc, je peux définir une classe de caractères pour les handles de fichiers retournés par la fonction CreateFile de Windows, comme illustré ici :

struct FileTraits
{
  static HANDLE Invalid() noexcept
  {
    return INVALID_HANDLE_VALUE;
  }
  static void Close(HANDLE value) noexcept
  {
    VERIFY(CloseHandle(value));
  }
};

La fonction CreateFile renvoie la valeur INVALID_HANDLE_VALUE si la fonction échoue. Dans le cas contraire, le handle résultant doit être fermé à l'aide de la fonction CloseHandle. C'est certes inhabituel. La fonction Windows CreateThreadpoolWork retourne un handle PTP_WORK pour représenter l'objet de travaux. C'est juste un pointeur opaque et une valeur nullptr est naturellement retournée si une erreur survient. Ainsi, une classe de caractères pour les objets de travail peut tirer parti de la modèle de classe HandleTraits, qui me permet d'économiser un peu de taper :

struct ThreadPoolWorkTraits : HandleTraits<PTP_WORK>
{
  static void Close(Type value) noexcept
  {
    CloseThreadpoolWork(value);
  }
};

Jusqu'à ce que le modèle de classe réels poignée ressemble ? Il peut bien, simplement comptent sur la classe de caractères donné, déduire le type de la poignée et appelez la méthode Close, selon les besoins. L'inférence prend la forme d'une expression decltype pour déterminer le type de la poignée :

template <typename Traits>
class Handle
{
  using Type = decltype(Traits::Invalid());
  Type m_value;
};

Cette approche empêche l'auteur de la classe de caractères d'avoir à inclure un alias de type ou un typedef pour fournir le type explicitement et de façon redondante. La poignée de fermeture est le premier ordre du jour et une méthode sécuritaire d'étroite assistance est rentrée dans la partie privée de la poignée de modèle de classe :

void Close() noexcept
{
  if (*this) // operator bool
  {
    Traits::Close(m_value);
  }
}

Cette méthode Close s'appuie sur un opérateur booléen explicit pour déterminer si la poignée doit être fermé avant d'appeler la classe de traits pour exécuter réellement l'opération. L'opérateur booléen explicite public constitue une autre amélioration sur mon modèle de classe du handle de 2011, en ce qu'elle peut simplement être implémentée comme un opérateur de conversion explicite :

explicit operator bool() const noexcept
{
  return m_value != Traits::Invalid();
}

Cela permet de résoudre toutes sortes de problèmes et est certainement beaucoup plus facile à définir que les approches traditionnelles qui implémentent un opérateur de type Boolean, tout en évitant les conversions implicites redoutées que le compilateur permettrait par ailleurs. Une autre amélioration de langue, dont j'ai déjà fait usage de dans cet article, est la capacité de supprimer explicitement des membres spéciaux, et je vais le faire que maintenant pour le constructeur de copie et un opérateur d'assignation de copie :

Handle(Handle const &) = delete;
Handle & operator=(Handle const &) = delete;

Un constructeur par défaut peut s'appuyer sur la classe de traits pour initialiser la poignée d'une manière prévisible :

explicit Handle(Type value = Traits::Invalid()) noexcept :
  m_value(value)
{}

Et le destructeur peut s'appuyer tout simplement sur le programme d'assistance étroite :

~Handle() noexcept
{
  Close();
}

Donc, les copies ne sont pas autorisés, mais mis à part le verrou SRW, je ne peux pas penser une ressource handle qui ne permet pas de déplacer sa poignée dans la mémoire. La possibilité de déplacer les poignées est extrêmement pratique. Poignées de déplacement implique deux opérations individuelles qui je pourrais désigner comme détacher et attacher, ou peut-être se détacher et réinitialiser. Détacher consiste à libérer l'appropriation de la poignée à l'appelant :

Type Detach() noexcept
{
  Type value = m_value;
  m_value = Traits::Invalid();
  return value;
}

La valeur du handle est retournée à l'appelant et la copie de l'objet handle est non valide pour s'assurer que son destructeur n'appelle pas la méthode Close, fournie par la classe de caractères. La fixation complémentaire ou réinitialisation implique fermant toute poignée existante et ensuite en supposant que la propriété d'une nouvelle valeur de la poignée :

bool Reset(Type value = Traits::Invalid()) noexcept
{
  Close();
  m_value = value;
  return static_cast<bool>(*this);
}

La méthode Reset par défaut la valeur non valide de la poignée et devient un moyen simple pour fermer une poignée prématurément. Elle retourne également le résultat de l'opérateur booléen explicite dans un but pratique. J'ai trouvé moi-même écrit le contour assez régulièrement :

work.Reset(CreateThreadpoolWork( ... ));
if (work)
{
  // Work object created successfully
}

Ici, je me fonde sur l'opérateur booléen explicite pour vérifier la validité de la poignée après coup. Être capable de se condenser cela en une seule expression peut être très pratique :

if (work.Reset(CreateThreadpoolWork( ... )))
{
  // Work object created successfully
}

Maintenant que j'ai cette poignée de main en place que je peux implémenter les opérations de déplacement tout simplement, en commençant par le constructeur du déménagement :

Handle(Handle && other) noexcept :
  m_value(other.Detach())
{}

La méthode de détachement est appelée sur la référence rvalue et la poignée nouvellement construite vole effectivement propriété loin de l'autre objet de la poignée. L'opérateur d'assignation de déménagement est seulement un peu plus compliquée :

Handle & operator=(Handle && other) noexcept
{
  if (this != &other)
  {
    Reset(other.Detach());
  }
  return *this;
}

Un contrôle d'identité est d'abord effectué pour éviter d'y attacher une poignée fermée. La méthode Reset sous-jacente ne dérange pas pour effectuer ce type de vérification, car cela impliquerait deux branches supplémentaires pour chaque affectation de déménagement. On est prudent. Deux est redondant. Bien sûr, déplacer la sémantique est grande, mais swap sémantique est encore mieux, surtout si vous enregistrerai poignées dans des conteneurs standards :

void Swap(Handle<Traits> & other) noexcept
{
  Type temp = m_value;
  m_value = other.m_value;
  other.m_value = temp;
}

Naturellement, une fonction non membre et minuscules swap est requise pour la généricité :

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

La touche finale pour le modèle de classe du Handle se présentent sous la forme d'une paire de méthodes Get et Set. Get est évident :

Type Get() const noexcept
{
  return m_value;
}

Elle retourne simplement la valeur du handle sous-jacent, qui peut être nécessaire de passer à diverses fonctions de la bibliothèque. L'ensemble est peut-être moins évident :

Type * Set() noexcept
{
  ASSERT(!*this);
  return &m_value;
}

Il s'agit d'une opération ensembliste indirecte. L'assertion souligne ce fait. Dans le passé, j'ai appelé cette GetAddressOf, mais ce nom se déguise ou contredit son but réel. Une telle opération set indirecte est nécessaire dans les cas où la bibliothèque retourne un handle comme paramètre de sortie. La fonction WindowsCreateString est qu'un exemple parmi tant d'autres :

HSTRING string = nullptr;
HRESULT hr = WindowsCreateString( ... , &string);

Je pourrais appeler les WindowsCreateString de cette façon et puis attacher le handle résultant à un objet de poignée, ou je peux simplement utiliser la méthode Set pour assumer la propriété directement :

Handle<StringTraits> string;
HRESULT hr = WindowsCreateString( ... , string.Set());

C'est beaucoup plus fiable et indique clairement la direction dans laquelle les données s'écoule. Le modèle de classe du Handle fournit également les opérateurs de comparaison habituels, mais grâce au soutien de la langue pour les opérateurs de conversion explicite, ce ne sont plus nécessaires pour éviter la conversion implicite. Ils seront juste utiles, mais je laisse ça à explorer. Le modèle de classe du Handle 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 possède un blog sur kennykerr.ca et vous pouvez le suivre sur Twitter à l'adresse twitter.com/kennykerr.