Remarque
L’accès à cette page nécessite une autorisation. Vous pouvez essayer de vous connecter ou de modifier des répertoires.
L’accès à cette page nécessite une autorisation. Vous pouvez essayer de modifier des répertoires.
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 entraîne la création d’un objet asynchrone, et cet objet a un pointeur implicite this (par le biais duquel m_value devient 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.
- En principal, une instance de MyClass est créée (
myclass_instance). - L’objet
asyncest créé et pointe (via son this) versmyclass_instance. - La fonction winrt ::Windows ::Foundation ::IAsyncAction ::get atteint son premier point de suspension, bloque pendant quelques secondes, puis retourne le résultat de RetrieveValueAsync.
-
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 s’attend naturellement à pouvoir utiliser son pointeur this.
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. Telle qu’elle est écrite, la coroutine contient effectivement un pointeur this brut qui pointe vers l’instance de classe. Toutefois, cela ne suffit pas à préserver l’instance de classe.
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 à partir de winrt::implements, et pour obtenir des 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 la stabilité de ses dépendances.
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 abordé des problèmes potentiels de durée de vie au niveau des coroutines et de l’accès concurrentiel. 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 la durée de vie du destinataire de l’événement dépasse celle de la source d’événements, elle dépasse également celle de 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 durée de vie de la page dépasse celle du bouton, par conséquent, la durée de vie du gestionnaire dépasse également celle du 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.
Quand il existe un cas où vous êtes certain que le gestionnaire ne survivra pas au this dont il dépend, vous pouvez capturer this normalement, sans tenir compte de la durée de vie forte 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, un événement en cours peut atteindre votre objet une fois qu’il a commencé sa destruction, même après une révocation (en particulier lorsque vous révoquez au sein du destructeur). 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 se produit après que le code du gestionnaire (ou de la continuation d’une coroutine) a tenté 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 faible, comme nous allons l’expliquer ci-dessous.
Ou, si cela a du sens pour votre scénario et si les considérations liées au thread le rendent même possible, alors une autre option consiste à révoquer le gestionnaire une fois que le destinataire en a terminé avec l’événement, ou dans le destructeur du destinataire. Voir Révoquer un délégué inscrit.
Voici comment nous allons inscrire 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 à partir de winrt::implements, et pour obtenir des 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énements des fonctions membres potentiellement dangereuses, qui utilise un pointeur this brut.
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 deviendra éligible pour la destruction uniquement lorsque l’inscription du gestionnaire sera annulée et que tous les rappels en attente seront retourné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 conserve une référence faible. À la dernière minute, et en arrière-plan, le délégué tente de résoudre la référence faible en une référence forte, et n’appelle la fonction membre qu’en cas de réussite.
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, votre gestionnaire retourne aux points d’interruption s’il est asynchrone. Vous devez donc fournir à votre coroutine une référence forte à l’instance de classe avant le premier point d’interruption. Une fois de plus, pour plus d’informations, consultez la section Accès sécurisé au pointeur this dans une coroutine de 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 utilisant 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 à this peut être obtenue, alors 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. Par conséquent, C++/WinRT fournit la prise en charge des références faibles automatiquement sur le modèle de struct winrt::implements, à partir duquel vos propres types C++/WinRT dérivent directement ou indirectement. Il s’agit d’une prise en charge de type « pay-for-play », dans la mesure où elle 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 refusé, le compilateur vous aidera en affichant une erreur « This is only for weak ref support » (Ceci est réservé à la prise en charge des références faibles).
API importantes
Windows developer