Partager via


Diagnostic des allocations directes

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

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.

Mise en scène, avec MyStringable

Tout d’abord, prenons en compte une implémentation simple de 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 MyStringable n’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 d’utiliser 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 n’a compilé avec C++/WinRT 1.0, en raison de cette conversion implicite. Le problème (très grave) est que nous renvoyons potentiellement un type projeté qui pointe vers un objet compté par référence dont la mémoire de stockage 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 dangereuse et gourmande en main-d’œuvre de bogues. Ne les utilisez pas si vous n’avez pas besoin de le faire. C++/WinRT fait tout son possible pour rendre les opérations efficaces sans jamais vous forcer à utiliser des pointeurs non encapsulés. 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. Windows Runtime (et COM classique avant) 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 classiques). 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 une 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 dérivant de l'implémentation via une classe final qui fournit la capacité de remplacement. 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 qui 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 des fonctions virtuelles dans votre implémentation comme 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 distribution virtuelle est limitée à l’interface binaire d’application (ABI) (qui est toujours final), et vos méthodes d’implémentation s’appuient sur le polymorphisme statique ou au moment de la compilation. 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 conduit à un inlining beaucoup 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 populaires avec les implémentations COM classiques, car, encore une fois, tout était virtuel, et il était courant de traiter directement avec des pointeurs bruts et donc était facile d’appeler accidentellement delete au lieu de release. C++/WinRT s'efforce de rendre difficile l'utilisation directe de pointeurs bruts. Et vous devrez vraiment sortir de votre chemin pour obtenir un pointeur brut en C++/WinRT sur lequel vous pourriez potentiellement appeler delete. 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 Windows Runtime. Il ne doit pas s’agir du code que vous écrivez tous les jours. Plutôt, C++/WinRT vous permet d’écrire du code plus semblable au C++ moderne, et beaucoup moins comme le COM classique.

API importantes