Diagnostic des allocations directes

Comme expliqué dans les API Author avec C++/WinRT, lorsque vous créez un objet de type d’implémentation, vous devez utiliser la famille d’assistance winrt ::make pour ce faire. Cette rubrique traite en détail d’une fonctionnalité de C++/WinRT 2.0 qui vous aide à diagnostiquer l’erreur consistant à allouer directement sur la pile un objet de type d’implémentation.

Ces erreurs peuvent se transformer en incidents mystérieux ou corruptions difficiles et fastidieuses à déboguer. Il s’agit donc d’une fonctionnalité importante, et il vaut la peine de comprendre l’arrière-plan.

Définition de la scène, avec MyStringable

Tout d’abord, prenons en compte une implémentation simple d’IStringable.

struct MyStringable : implements<MyStringable, IStringable>
{
    winrt::hstring ToString() const { return L"MyStringable"; }
};

Imaginez maintenant que vous devez appeler une fonction (à partir de votre implémentation) qui attend un IStringable en tant qu’argument.

void Print(IStringable const& stringable)
{
    printf("%ls\n", stringable.ToString().c_str());
}

Le problème est que notre type MyStringablen’est pas un IStringable.

  • Notre type MyStringable est une implémentation de l’interface IStringable .
  • Le type IStringable est un type projeté.

Important

Il est important de comprendre la distinction entre un type d’implémentation et un type projeté. Pour les concepts et termes essentiels, veillez à lire Consommer des API avec C++/WinRT et créer des API avec C++/WinRT.

L’espace entre une implémentation et la projection peut être subtil à saisir. En fait, pour essayer de rendre l’implémentation un peu plus semblable à la projection, l’implémentation fournit des conversions implicites à chacun des types projetés qu’il implémente. Cela ne veut pas dire que nous pouvons simplement le faire.

struct MyStringable : implements<MyStringable, IStringable>
{
    winrt::hstring ToString() const;
 
    void Call()
    {
        Print(this);
    }
};

Au lieu de cela, nous devons obtenir une référence afin que les opérateurs de conversion puissent être utilisés comme candidats à la résolution de l’appel.

void Call()
{
    Print(*this);
}

Ça marche. Une conversion implicite fournit une conversion (très efficace) du type d’implémentation au type projeté, et c’est très pratique pour de nombreux scénarios. Sans cette facilité, beaucoup de types d’implémentation s’avéreraient très fastidieux à créer. À condition que vous utilisiez uniquement le modèle de fonction winrt ::make (ou winrt ::make_self) pour allouer l’implémentation, tout est bien.

IStringable stringable{ winrt::make<MyStringable>() };

Pièges potentiels avec C++/WinRT 1.0

Toutefois, les conversions implicites peuvent vous poser des problèmes. Considérez cette fonction d’assistance inutile.

IStringable MakeStringable()
{
    return MyStringable(); // Incorrect.
}

Ou même juste cette déclaration apparemment inoffensive.

IStringable stringable{ MyStringable() }; // Also incorrect.

Malheureusement, le code comme celui-ci a été compilé avec C++/WinRT 1.0, en raison de cette conversion implicite. Le problème (très sérieux) est que nous risquons de renvoyer un type projeté qui pointe vers un objet avec comptage de références dont la mémoire sous-jacente se trouve sur la pile éphémère.

Voici un autre élément compilé avec C++/WinRT 1.0.

MyStringable* stringable{ new MyStringable() }; // Very inadvisable.

Les pointeurs bruts sont une source de bogues dangereuse et fastidieuse à gérer. Ne les utilisez pas si vous n’avez pas besoin de le faire. C++/WinRT fait tout son possible pour garantir une efficacité optimale sans jamais vous obliger à utiliser des pointeurs bruts. Voici un autre élément compilé avec C++/WinRT 1.0.

auto stringable{ std::make_shared<MyStringable>(); } // Also very inadvisable.

C’est une erreur à plusieurs niveaux. Nous avons deux nombres de références différents pour le même objet. Le Windows Runtime (et COM classique avant celui-ci) est basé sur un nombre de références intrinsèques qui n'est pas compatible avec std ::shared_ptr. std ::shared_ptr a bien sûr de nombreuses applications valides ; mais ce n'est pas nécessaire lorsque vous partagez des objets Windows Runtime (et COM classique). Enfin, cette opération a également été compilée avec C++/WinRT 1.0.

auto stringable{ std::make_unique<MyStringable>() }; // Highly dubious.

C’est encore une fois assez discutable. La propriété unique est opposée à la durée de vie partagée du nombre de références intrinsèques de MyStringable.

Solution avec C++/WinRT 2.0

Avec C++/WinRT 2.0, toutes ces tentatives d’allocation directe des types d’implémentation entraînent une erreur du compilateur. C’est le meilleur type d’erreur, et infiniment mieux qu’un bogue d’exécution mystérieux.

Chaque fois que vous devez effectuer une implémentation, vous pouvez simplement utiliser winrt ::make ou winrt ::make_self, comme indiqué ci-dessus. Et maintenant, si vous oubliez de le faire, vous serez accueilli avec une erreur du compilateur indiquant cela avec une référence à une fonction abstraite nommée use_make_function_to_create_this_object. Ce n’est pas exactement un static_assert; mais c’est proche. Toutefois, il s’agit du moyen le plus fiable de détecter toutes les erreurs décrites.

Cela signifie que nous devons placer quelques contraintes mineures sur l’implémentation. Étant donné que nous nous appuyons sur l’absence d’un remplacement pour détecter l’allocation directe, le modèle de fonction winrt ::make doit en quelque sorte satisfaire la fonction virtuelle abstraite avec un remplacement. Il le fait en héritant de l’implémentation avec une classe final qui fournit la redéfinition. Il y a quelques points à observer sur ce processus.

Tout d’abord, la fonction virtuelle est présente uniquement dans les builds de débogage. Cela signifie que la détection n’affectera pas la taille de la table virtuelle dans vos builds optimisées.

Deuxièmement, étant donné que la classe dérivée que winrt ::make utilise est final, cela signifie que toute dévirtualisation que l’optimiseur peut éventuellement déduire se produira même si vous avez choisi de ne pas marquer votre classe d’implémentation comme final. C’est donc une amélioration. L’inverse est que votre implémentation ne peut pas être final. Là encore, ce n’est pas une conséquence, car le type instancié sera toujours final.

Troisièmement, rien ne vous empêche de marquer les fonctions virtuelles dans votre implémentation en tant que final. Bien sûr, C++/WinRT est très différent de com classique et d’implémentations telles que WRL, où tout ce qui concerne votre implémentation a tendance à être virtuel. Dans C++/WinRT, la liaison dynamique est limitée à l’interface binaire d’application (ABI) (qui est toujours final), et vos méthodes d’implémentation reposent sur le polymorphisme à la compilation ou le polymorphisme statique. Cela évite le polymorphisme du runtime inutile et signifie également qu’il existe peu de raisons pour les fonctions virtuelles dans votre implémentation C++/WinRT. C’est une très bonne chose, et cela rend l’intégration en ligne bien plus prévisible.

Quatrièmement, étant donné que winrt ::make injecte une classe dérivée, votre implémentation ne peut pas avoir de destructeur privé. Les destructeurs privés étaient prisés dans les implémentations COM classiques parce que, là encore, tout était virtuel, et qu’il était courant de manipuler directement des pointeurs bruts ; il était donc facile d’appeler accidentellement delete au lieu de Release. C++/WinRT fait tout son possible pour vous empêcher de manipuler directement des pointeurs bruts. Et vous devrez vraiment sortir de votre chemin pour obtenir un pointeur brut en C++/WinRT que vous pourriez appeler delete potentiellement. La sémantique des valeurs signifie que vous traitez de valeurs et de références ; et rarement avec des pointeurs.

Ainsi, C++/WinRT défie nos notions préconçues de ce qu’il signifie pour écrire du code COM classique. Et c’est parfaitement raisonnable, car WinRT n’est pas com classique. Com classique est le langage d’assembly de la Windows Runtime. Il ne doit pas s’agir du code que vous écrivez tous les jours. À l’inverse, C++/WinRT vous permet d’écrire un code davantage proche du C++ moderne, et bien moins du COM classique.

API importantes