Références fortes et faibles en C++/WinRT

Important

Construire avec le SDK d'application Windows ? Le code de cet article utilise des espaces de noms UWP (Windows.UI.Xaml). Si votre projet cible WinUI 3 (SDK d'application Windows), remplacez Microsoft.UI.Xaml (et les espaces de noms associésMicrosoft.UI.*) dans l’ensemble. Pour plus d’informations, consultez mappage des API UWP à l’SDK d'application Windows pour obtenir un guide complet de mappage et de migration de l’interface utilisateur.

Le Windows Runtime est un système à comptage de références ; dans un tel système, il est important de comprendre l'importance des références fortes et faibles, ainsi que la distinction entre elles (et des références qui ne sont ni l'une ni l'autre, comme le pointeur implicite this). Comme vous le verrez dans cette rubrique, savoir comment gérer ces références correctement peut signifier la différence entre un système fiable qui s’exécute correctement et un système qui se bloque de façon imprévisible. En fournissant des fonctions utilitaires bénéficiant d’une prise en charge poussée dans la projection de langage, C++/WinRT vous facilite la tâche pour créer des systèmes plus complexes, simplement et correctement.

Note

À quelques exceptions près, la prise en charge des références faibles est activée par défaut pour les types du Windows Runtime que vous consommez ou définissez dans C++/WinRT. Windows. UI. Composition et Windows. Devices.Input.PenDevice sont des exemples d’exceptions, c’est-à-dire des espaces de noms où la prise en charge des références faibles n’est pas activée pour ces types. Voir aussi Si votre délégué de révocation automatique ne parvient pas à s’enregistrer.

Si vous créez des types, consultez la section Références faibles dans C++/WinRT de cette rubrique.

Accéder en toute sécurité au pointeur this dans une coroutine membre d’une classe

Pour plus d’informations sur les coroutines et les exemples de code, consultez Concurrence et opérations asynchrones avec C++/WinRT.

L’extrait de code ci-dessous présente un exemple typique de coroutine qui est une fonction membre d’une classe. Vous pouvez copier-coller cet exemple dans les fichiers spécifiés dans un nouveau projet d’application console Windows (C++/WinRT).

// pch.h
#pragma once
#include <iostream>
#include <winrt/Windows.Foundation.h>

// main.cpp : Defines the entry point for the console application.
#include "pch.h"

using namespace winrt;
using namespace Windows::Foundation;
using namespace std::chrono_literals;

struct MyClass : winrt::implements<MyClass, IInspectable>
{
    winrt::hstring m_value{ L"Hello, World!" };

    IAsyncOperation<winrt::hstring> RetrieveValueAsync()
    {
        co_await 5s;
        co_return m_value;
    }
};

int main()
{
    winrt::init_apartment();

    auto myclass_instance{ winrt::make_self<MyClass>() };
    auto async{ myclass_instance->RetrieveValueAsync() };

    winrt::hstring result{ async.get() };
    std::wcout << result.c_str() << std::endl;
}

MyClass ::RetrieveValueAsync passe un certain temps à travailler et retourne finalement une copie du MyClass::m_value membre de données. L’appel de RetrieveValueAsync provoque la création d’un objet asynchrone, et cet objet a implicitement ce pointeur (auquel, finalement, m_value est accessible).

N’oubliez pas que, dans une coroutine, l’exécution est synchrone jusqu’au premier point de suspension, où le contrôle est retourné à l’appelant. Dans RetrieveValueAsync, le premier co_await est le premier point de suspension. Au moment où la coroutine reprend (environ cinq secondes plus tard, dans ce cas), il a pu arriver n’importe quoi au pointeur implicite this via lequel nous accédons à m_value.

Voici la séquence complète d’événements.

  1. En principal, une instance de MyClass est créée (myclass_instance).
  2. L’objet async est créé et pointe (via son this) vers myclass_instance.
  3. La fonction winrt ::Windows ::Foundation ::IAsyncAction ::get atteint son premier point de suspension, bloque pendant quelques secondes, puis retourne le résultat de RetrieveValueAsync.
  4. RetrieveValueAsync retourne la valeur de this->m_value.

L’étape 4 est sécurisée uniquement tant que cela reste valide.

Mais que se passe-t-il si l’instance de classe est détruite avant la fin de l’opération asynchrone ? Il existe toutes sortes de façons dont l’instance de classe peut sortir de portée avant la fin de la méthode asynchrone. Mais nous pouvons le simuler en définissant l’instance de classe sur nullptr.

int main()
{
    winrt::init_apartment();

    auto myclass_instance{ winrt::make_self<MyClass>() };
    auto async{ myclass_instance->RetrieveValueAsync() };
    myclass_instance = nullptr; // Simulate the class instance going out of scope.

    winrt::hstring result{ async.get() }; // Behavior is now undefined; crashing is likely.
    std::wcout << result.c_str() << std::endl;
}

Après avoir détruit l’instance de la classe, il semble que nous n’y fassions plus directement référence par la suite. Mais bien sûr, l’objet asynchrone possède un pointeur this vers lui-même et tente de l’utiliser pour copier la valeur stockée dans l’instance de la classe. La coroutine est une fonction membre, et elle suppose pouvoir utiliser son pointeur this en toute impunité.

Avec cette modification du code, nous avons rencontré un problème à l’étape 4, car l’instance de classe a été détruite et ce n’est plus valide. Dès que l’objet asynchrone tente d’accéder à la variable à l’intérieur de l’instance de classe, il se bloque (ou fait quelque chose d’entièrement non défini).

La solution consiste à donner à l’opération asynchrone ( la coroutine) sa propre référence forte à l’instance de classe. Comme actuellement écrit, la coroutine contient efficacement un pointeur brut vers l’instance de classe ; mais ce n’est pas suffisant pour maintenir l’instance de classe active.

Pour maintenir l’instance de classe active, remplacez l’implémentation de RetrieveValueAsync par celle indiquée ci-dessous.

IAsyncOperation<winrt::hstring> RetrieveValueAsync()
{
    auto strong_this{ get_strong() }; // Keep *this* alive.
    co_await 5s;
    co_return m_value;
}

Une classe C++/WinRT dérive directement ou indirectement du modèle winrt ::implements . De ce fait, l’objet C++/WinRT peut appeler sa fonction membre protégée implements::get_strong pour obtenir une référence forte vers son pointeur this. Notez qu’il n’est pas nécessaire d’utiliser réellement la strong_this variable dans l’exemple de code ci-dessus. Il suffit d’appeler get_strong incrémente le nombre de références de l’objet C++/WinRT et conserve son pointeur implicite valide.

Important

Étant donné que get_strong est une fonction membre du modèle de structure winrt::implements, vous pouvez l’appeler uniquement depuis une classe qui dérive directement ou indirectement de winrt::implements, telle qu’une classe C++/WinRT. Pour plus d’informations sur la dérivation de winrt ::implements et d’exemples, consultez Créer des API avec C++/WinRT.

Cela résout le problème que nous avions précédemment à l’étape 4. Même si toutes les autres références à l’instance de classe disparaissent, la coroutine a pris la précaution de garantir que ses dépendances sont stables.

Si une référence forte n’est pas appropriée, vous pouvez sinon appeler implements::get_weak pour récupérer une référence faible vers this. Vérifiez simplement que vous pouvez récupérer une référence forte avant d’y accéder. Là encore, get_weak est une fonction membre du modèle de struct winrt ::implements .

IAsyncOperation<winrt::hstring> RetrieveValueAsync()
{
    auto weak_this{ get_weak() }; // Maybe keep *this* alive.

    co_await 5s;

    if (auto strong_this{ weak_this.get() })
    {
        co_return m_value;
    }
    else
    {
        co_return L"";
    }
}

Dans l’exemple ci-dessus, la référence faible ne empêche pas l’instance de classe d’être détruite lorsqu’aucune référence forte ne reste. Toutefois, il vous permet de vérifier si une référence forte peut être acquise avant d’accéder à la variable membre.

Accès sécurisé au pointeur this à l’aide d’un délégué de gestion d’événements

Scénario

Pour obtenir des informations générales sur la gestion des événements, consultez Gérer les événements à l’aide de délégués en C++/WinRT.

La section précédente a mis en évidence des problèmes potentiels liés à la durée de vie dans les domaines des coroutines et de la concurrence. Toutefois, si vous gérez un événement avec la fonction membre d’un objet ou à partir d’une fonction lambda à l’intérieur de la fonction membre d’un objet, vous devez réfléchir aux durées de vie relatives du destinataire de l’événement (l’objet qui gère l’événement) et à la source d’événement (l’objet qui déclenche l’événement). Examinons quelques exemples de code.

La liste de code ci-dessous définit d’abord une classe EventSource simple, qui déclenche un événement générique géré par tous les délégués qui ont été ajoutés à celui-ci. Cet exemple d’événement se produit pour utiliser le type de délégué Windows ::Foundation ::EventHandler, mais les problèmes et solutions ici s’appliquent à tous les types délégués.

Ensuite, la classe EventRecipient fournit un gestionnaire pour l’événement EventSource ::Event sous la forme d’une fonction lambda.

// pch.h
#pragma once
#include <iostream>
#include <winrt/Windows.Foundation.h>

// main.cpp : Defines the entry point for the console application.
#include "pch.h"

using namespace winrt;
using namespace Windows::Foundation;

struct EventSource
{
    winrt::event<EventHandler<int>> m_event;

    void Event(EventHandler<int> const& handler)
    {
        m_event.add(handler);
    }

    void RaiseEvent()
    {
        m_event(nullptr, 0);
    }
};

struct EventRecipient : winrt::implements<EventRecipient, IInspectable>
{
    winrt::hstring m_value{ L"Hello, World!" };

    void Register(EventSource& event_source)
    {
        event_source.Event([&](auto&& ...)
        {
            std::wcout << m_value.c_str() << std::endl;
        });
    }
};

int main()
{
    winrt::init_apartment();

    EventSource event_source;
    auto event_recipient{ winrt::make_self<EventRecipient>() };
    event_recipient->Register(event_source);
    event_source.RaiseEvent();
}

Le schéma consiste en ceci : le récepteur de l’événement possède un gestionnaire d’événement lambda qui dépend de son pointeur this. Chaque fois que le destinataire de l’événement survit à la source de l’événement, il survit à ces dépendances. Et dans ces cas, qui sont communs, le modèle fonctionne bien. Certains de ces cas sont évidents, par exemple lorsqu’une page d’interface utilisateur gère un événement déclenché par un contrôle qui se trouve sur la page. La page survit au bouton ; le gestionnaire d’événement survit donc lui aussi au bouton. Cela est vrai chaque fois que l’objet destinataire possède l’objet source (comme membre de données, par exemple), ou chaque fois que l’objet destinataire et l’objet source sont des objets frères appartenant directement à un même autre objet.

Lorsque vous êtes sûr d’être dans un cas où le gestionnaire ne survivra pas au this dont il dépend, vous pouvez alors capturer this normalement, sans vous préoccuper d’un cycle de vie fort ou faible.

Toutefois, il existe toujours des cas où cela n’a pas dépassé son utilisation dans un gestionnaire (y compris les gestionnaires pour les événements d’achèvement et de progression déclenchés par des actions et des opérations asynchrones), et il est important de savoir comment les traiter.

  • Lorsqu’une source d’événement déclenche ses événements de façon synchrone, vous pouvez révoquer votre gestionnaire et être certain que vous ne recevrez plus d’événements. Toutefois, pour les événements asynchrones, même après la révocation (et en particulier lors de la révocation dans le destructeur), un événement en cours d’exécution peut atteindre votre objet une fois qu’il a commencé à détruire. Trouver où se désabonner avant la destruction peut atténuer le problème, mais poursuivez votre lecture pour découvrir une solution robuste.
  • Si vous créez une coroutine pour implémenter une méthode asynchrone, il est possible.
  • Dans de rares cas, avec certains objets du framework d’interface utilisateur XAML (SwapChainPanel, par exemple), cela est possible si le récepteur est finalisé sans se désinscrire de la source d’événement.

Le problème

La version suivante de la fonction main simule ce qui se passe lorsque le destinataire de l’événement est détruit (par exemple, s’il sort de portée) alors que la source d’événements continue de déclencher des événements.

int main()
{
    winrt::init_apartment();

    EventSource event_source;
    auto event_recipient{ winrt::make_self<EventRecipient>() };
    event_recipient->Register(event_source);
    event_recipient = nullptr; // Simulate the event recipient going out of scope.
    event_source.RaiseEvent(); // Behavior is now undefined within the lambda event handler; crashing is likely.
}

Le récepteur de l’événement est détruit, mais le gestionnaire d’événement lambda qu’il contient est toujours abonné à l’événement Event. Lorsqu’un tel événement est émis, l’expression lambda tente de déréférencer le pointeur this, qui n’est alors plus valide. Par conséquent, une violation d’accès résulte du code dans le gestionnaire (ou dans la continuation d’une coroutine) qui tente de l’utiliser.

Important

Si vous rencontrez une situation comme celle-ci, vous devez réfléchir à la durée de vie de cet objet ; et si l’objet capturé a dépassé ou non la capture. Si ce n’est pas le cas, capturez-le avec une référence forte ou une référence faible, comme nous le montrerons ci-dessous.

Ou, si cela se justifie dans votre scénario et si les contraintes liées aux threads le permettent, une autre possibilité consiste à révoquer le gestionnaire une fois que le destinataire a fini de traiter l’événement, ou dans le destructeur du destinataire. Voir Révoquer un délégué inscrit.

C’est ainsi que nous enregistrons le gestionnaire.

event_source.Event([&](auto&& ...)
{
    std::wcout << m_value.c_str() << std::endl;
});

Le lambda capture automatiquement toutes les variables locales par référence. Par conséquent, pour cet exemple, nous pourrions avoir écrit cela de façon équivalente.

event_source.Event([this](auto&& ...)
{
    std::wcout << m_value.c_str() << std::endl;
});

Dans les deux cas, nous capturons simplement le pointeur brut this. Et cela n’a aucun effet sur le comptage des références, donc rien n’empêche l’objet actuel d’être détruit.

La solution

La solution consiste à capturer une référence forte (ou, comme nous le verrons, une référence faible si cela est plus approprié). Une référence forte incrémente le nombre de références et conserve l’objet actuel actif. Vous devez simplement déclarer une variable de capture (appelée strong_this dans cet exemple) et l’initialiser avec un appel à implémenter ::get_strong, qui récupère une référence forte à notre pointeur.

Important

Étant donné que get_strong est une fonction membre du modèle de structure winrt::implements, vous pouvez l’appeler uniquement depuis une classe qui dérive directement ou indirectement de winrt::implements, telle qu’une classe C++/WinRT. Pour plus d’informations sur la dérivation de winrt ::implements et d’exemples, consultez Créer des API avec C++/WinRT.

event_source.Event([this, strong_this { get_strong()}](auto&& ...)
{
    std::wcout << m_value.c_str() << std::endl;
});

Vous pouvez même omettre la capture automatique de l’objet courant et accéder au membre de données via la variable de capture plutôt que via l’implicite this.

event_source.Event([strong_this { get_strong()}](auto&& ...)
{
    std::wcout << strong_this->m_value.c_str() << std::endl;
});

Si une référence forte n’est pas appropriée, vous pouvez sinon appeler implements::get_weak pour récupérer une référence faible vers this. Une référence faible ne conserve pas l’objet actuel actif. Par conséquent, vérifiez simplement que vous pouvez toujours récupérer une référence forte à partir de la référence faible avant d’accéder aux membres.

event_source.Event([weak_this{ get_weak() }](auto&& ...)
{
    if (auto strong_this{ weak_this.get() })
    {
        std::wcout << strong_this->m_value.c_str() << std::endl;
    }
});

Si vous capturez un pointeur brut, vous devez vous assurer que vous conservez l’objet pointu actif.

Si vous utilisez une fonction membre en tant que délégué

Ainsi que les fonctions lambda, ces principes s’appliquent également à l’utilisation d’une fonction membre en tant que délégué. La syntaxe est différente. Nous allons donc examiner du code. Tout d’abord, voici le gestionnaire d’événement d’une fonction membre potentiellement dangereux, utilisant un pointeur brut this.

struct EventRecipient : winrt::implements<EventRecipient, IInspectable>
{
    winrt::hstring m_value{ L"Hello, World!" };

    void Register(EventSource& event_source)
    {
        event_source.Event({ this, &EventRecipient::OnEvent });
    }

    void OnEvent(IInspectable const& /* sender */, int /* args */)
    {
        std::wcout << m_value.c_str() << std::endl;
    }
};

Il s’agit de la méthode standard et conventionnelle pour faire référence à un objet et à sa fonction membre. Pour garantir cette sécurité, vous pouvez, à partir de la version 10.0.17763.0 (Windows 10, version 1809) du SDK Windows, établir une référence forte ou faible au moment où le gestionnaire est inscrit. À ce stade, l’objet destinataire de l’événement est connu pour être toujours actif.

Pour une référence forte, appelez simplement get_strong au lieu du pointeur brut this. C++/WinRT garantit que le délégué résultant contient une référence forte à l’objet actuel.

event_source.Event({ get_strong(), &EventRecipient::OnEvent });

Capturer une référence forte signifie que votre objet ne pourra être détruit qu’après la désinscription du gestionnaire et une fois que tous les rappels en cours se sont terminés. Toutefois, cette garantie est valide uniquement au moment où l’événement est déclenché. Si votre gestionnaire d’événements est asynchrone, vous devez alors donner à votre coroutine une référence forte à l’instance de la classe avant le premier point de suspension (pour plus d’informations et pour obtenir le code, consultez la section Accéder en toute sécurité au pointeur this dans une coroutine membre de classe, plus haut dans cette rubrique). Toutefois, cela crée une référence circulaire entre la source d’événement et votre objet. Vous devez donc l’interrompre explicitement en révoquant votre événement.

Pour une référence faible, appelez get_weak. C++/WinRT garantit que le délégué résultant contient une référence faible. Au dernier moment, et en coulisses, le délégué tente de convertir la référence faible en une référence forte, et n'appelle la fonction membre que si l'opération réussit.

event_source.Event({ get_weak(), &EventRecipient::OnEvent });

Si le délégué appelle votre fonction membre, C++/WinRT conserve votre objet actif jusqu’à ce que votre gestionnaire retourne. Toutefois, si votre gestionnaire est asynchrone, il retourne à des points de suspension, et vous devrez donc donner à votre coroutine une référence forte à l’instance de classe avant le premier point de suspension. Là encore, pour plus d’informations, consultez la section Accéder en toute sécurité au pointeur this dans une coroutine membre de classe, plus haut dans cette rubrique.

Si la fonction membre n'appartient pas à un type Windows Runtime

Lorsque la méthode get_strong n'est pas disponible pour vous (votre type n'est pas un type Windows Runtime), vous pouvez utiliser la technique indiquée dans l'exemple de code ci-dessous. Ici, une classe C++ standard (nommée ConsoleNetworkWatcher) s’affiche pour gérer l’événement NetworkInformation.NetworkStatusChanged .

#include <winrt/Windows.Networking.Connectivity.h>
using namespace winrt;
using namespace Windows::Networking::Connectivity;

class ConsoleNetworkWatcher
{
    /* any constructor, and instance methods, here*/

    static void Initialize(std::shared_ptr<ConsoleNetworkWatcher> instance)
    {
        auto weakPointer{ std::weak_ptr{ instance } };

        instance->m_statusChangedRevoker =
            NetworkInformation::NetworkStatusChanged(winrt::auto_revoke,
                [weakPointer](winrt::Windows::Foundation::IInspectable const& sender)
                {
                    auto sharedPointer{ weakPointer.lock() };

                    if (sharedPointer)
                    {
                        sharedPointer->NetworkStatusChanged(sender);
                    }
                });
    }

    void NetworkStatusChanged(winrt::Windows::Foundation::IInspectable const& sender){/* handle event here */};

private:
    NetworkInformation::NetworkStatusChanged_revoker m_statusChangedRevoker;
};

Exemple de référence faible utilisant SwapChainPanel ::CompositionScaleChanged

Dans cet exemple de code, nous utilisons l’événement SwapChainPanel ::CompositionScaleChanged à l’aide d’une autre illustration de références faibles. Le code inscrit un gestionnaire d’événements à l’aide d’une expression lambda qui capture une référence faible au destinataire.

winrt::Microsoft::UI::Xaml::Controls::SwapChainPanel m_swapChainPanel;
winrt::event_token m_compositionScaleChangedEventToken;

void RegisterEventHandler()
{
    m_compositionScaleChangedEventToken = m_swapChainPanel.CompositionScaleChanged([weak_this{ get_weak() }]
        (Microsoft::UI::Xaml::Controls::SwapChainPanel const& sender,
        Windows::Foundation::IInspectable const& object)
    {
        if (auto strong_this{ weak_this.get() })
        {
            strong_this->OnCompositionScaleChanged(sender, object);
        }
    });
}

void OnCompositionScaleChanged(Microsoft::UI::Xaml::Controls::SwapChainPanel const& sender,
    Windows::Foundation::IInspectable const& object)
{
    // Here, we know that the "this" object is valid.
}

Dans la clause de capture lamba, une variable temporaire est créée, représentant une référence faible à ceci. Dans le corps de l’expression lambda, si une référence forte à ceci peut être obtenue, la fonction OnCompositionScaleChanged est appelée. Ainsi, dans OnCompositionScaleChanged, cela peut être utilisé en toute sécurité.

Références faibles en C++/WinRT

Ci-dessus, nous avons vu des références faibles être utilisées. En général, ils sont bons pour briser les références cycliques. Par exemple, pour l’implémentation native de l’infrastructure d’interface utilisateur BASÉE sur XAML, en raison de la conception historique de l’infrastructure, le mécanisme de référence faible en C++/WinRT est nécessaire pour gérer les références cycliques. En dehors de XAML, cependant, vous n’aurez probablement pas besoin d’utiliser des références faibles (pas qu’il n’y a rien d’intrinsèquement propre à XAML à leur sujet). Au lieu de cela, vous devez, plus souvent que non, être en mesure de concevoir vos propres API C++/WinRT de manière à éviter la nécessité de références cycliques et de références faibles.

Pour tout type donné que vous déclarez, il n’est pas immédiatement évident pour C++/WinRT si ou lorsque des références faibles sont nécessaires. C++/WinRT fournit donc automatiquement une prise en charge des références faibles pour le modèle de structure winrt::implements, dont vos propres types C++/WinRT dérivent directement ou indirectement. C’est payant, dans le fait qu’il ne vous coûte rien, sauf si votre objet est réellement interrogé pour IWeakReferenceSource. Et vous pouvez choisir explicitement de refuser ce support.

Exemples de code

Le modèle de struct winrt ::weak_ref est une option permettant d’obtenir une référence faible à une instance de classe.

Class c;
winrt::weak_ref<Class> weak{ c };

Ou bien, vous pouvez utiliser la fonction utilitaire winrt::make_weak.

Class c;
auto weak = winrt::make_weak(c);

La création d’une référence faible n’affecte pas le nombre de références sur l’objet lui-même ; il provoque simplement l’allocation d’un bloc de contrôle. Ce bloc de contrôle s’occupe de l’implémentation de la sémantique de référence faible. Vous pouvez ensuite essayer de promouvoir la référence faible à une référence forte et, si elle réussit, utilisez-la.

if (Class strong = weak.get())
{
    // use strong, for example strong.DoWork();
}

À condition que d’autres références fortes existent toujours, l’appel weak_ref ::get incrémente le nombre de références et retourne la référence forte à l’appelant.

Refus de la prise en charge des références faibles

La prise en charge des références faibles est automatique. Toutefois, vous pouvez choisir explicitement de désactiver cette prise en charge en passant la structure marqueur winrt::no_weak_ref comme paramètre de modèle à votre classe de base.

Si vous dérivez directement de winrt::implements.

struct MyImplementation: implements<MyImplementation, IStringable, no_weak_ref>
{
    ...
}

Si vous créez une classe runtime.

struct MyRuntimeClass: MyRuntimeClassT<MyRuntimeClass, no_weak_ref>
{
    ...
}

Peu importe où se trouve la structure marqueur dans le pack de paramètres variadique. Si vous demandez une référence faible pour un type qui n’est pas compatible avec cette option, le compilateur vous indiquera « Ceci est uniquement destiné à la prise en charge des références faibles ».

API importantes