Riferimenti sicuri e deboli in C++/WinRT

Importante

Costruire con il SDK per app di Windows? Il codice di questo articolo usa spazi dei nomi UWP (Windows.UI.Xaml). Se il progetto è destinato a WinUI 3 (SDK per app di Windows), sostituisci Microsoft.UI.Xaml (e i namespace correlati Microsoft.UI.*) in tutto il progetto. Per altri dettagli, vedi Mapping delle API UWP alle SDK per app di Windows per un mapping completo e una guida alla migrazione dell'interfaccia utente.

Il Windows Runtime è un sistema con conteggio dei riferimenti; e in un sistema di questo tipo è importante conoscere il significato di e la distinzione tra riferimenti sicuri e deboli (e riferimenti che non sono entrambi, ad esempio il puntatore implicito). Come si vedrà in questo argomento, sapere come gestire questi riferimenti correttamente può significare la differenza tra un sistema affidabile che viene eseguito senza problemi e uno che si arresta in modo imprevedibile. Mettendo a disposizione funzioni di supporto con un'integrazione profonda nella proiezione del linguaggio, C++/WinRT ti viene incontro nel lavoro di realizzazione di sistemi più complessi in modo semplice e corretto.

Note

Con solo alcune eccezioni, il supporto dei riferimenti deboli è attivato per impostazione predefinita per i tipi Windows Runtime utilizzati o creati in C++/WinRT. Windows.UI.Composition e Windows.Devices.Input.PenDevice sono esempi di eccezioni, vale a dire spazi dei nomi in cui il supporto per i riferimenti deboli non è abilitato per tali tipi. Vedere anche Se il delegato di revoca automatica non riesce a eseguire la registrazione.

Se si creano tipi, vedere la sezione Riferimenti deboli in C++/WinRT in questo argomento.

Accesso sicuro al puntatore this in una coroutine membro di una classe

Per altre info sulle coroutine ed esempi di codice, vedi Concorrenza e operazioni asincrone con C++/WinRT.

L'elenco di codice seguente mostra un tipico esempio di coroutine che è una funzione membro di una classe. È possibile copiare incollare questo esempio nei file specificati in un nuovo progetto applicazione console di 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 impiega del tempo e alla fine restituisce una copia del MyClass::m_value membro dati. La chiamata a RetrieveValueAsync comporta la creazione di un oggetto asincrono, e tale oggetto ha un puntatore implicito this (attraverso il quale, alla fine, si accede a m_value).

Tenere presente che, in una coroutine, l'esecuzione è sincrona fino al primo punto di sospensione, dove il controllo viene restituito al chiamante. In RetrieveValueAsync, il primo co_await è il primo punto di sospensione. Quando la coroutine riprende (circa cinque secondi dopo, in questo caso), potrebbe essere successo di tutto al puntatore implicito this attraverso il quale accediamo a m_value.

Ecco la sequenza completa di eventi.

  1. In main viene creata un'istanza di MyClass (myclass_instance).
  2. L'oggetto async viene creato e punta (tramite this) a myclass_instance.
  3. La funzione winrt::Windows::Foundation::IAsyncAction::get raggiunge il primo punto di sospensione, blocca per alcuni secondi e quindi restituisce il risultato di RetrieveValueAsync.
  4. RetrieveValueAsync restituisce il valore di this->m_value.

Il passaggio 4 è sicuro solo finché questo rimane valido.

Ma cosa succede se l'istanza della classe viene distrutta prima che l'operazione asincrona sia completata? Esistono tutti i tipi di modi in cui l'istanza della classe potrebbe uscire dall'ambito prima del completamento del metodo asincrono. È tuttavia possibile simularlo impostando l'istanza della classe su 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;
}

Dopo il punto in cui distruggiamo l'istanza della classe, sembra che non vi facciamo più riferimento direttamente. Ma naturalmente l'oggetto asincrono ha un puntatore this a esso e tenta di usarlo per copiare il valore memorizzato nell'istanza della classe. La coroutine è una funzione membro e presuppone di poter usare senza problemi il puntatore this.

Con questa modifica al codice, si verifica un problema nel passaggio 4, perché l'istanza della classe è stata eliminata definitivamente e questa operazione non è più valida. Non appena l'oggetto asincrono tenta di accedere alla variabile all'interno dell'istanza della classe, si arresterà in modo anomalo (o eseguirà un'operazione completamente indefinita).

La soluzione consiste nell'assegnare all'operazione asincrona, ovvero la coroutine, il proprio riferimento sicuro all'istanza della classe. Così com'è attualmente scritta, la coroutine mantiene di fatto un puntatore raw this all'istanza della classe; ma questo non basta a mantenere in vita l'istanza della classe.

Per mantenere attiva l'istanza della classe, modificare l'implementazione di RetrieveValueAsync in quella illustrata di seguito.

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

Una classe C++/WinRT deriva direttamente o indirettamente dal modello winrt::implements . Per questo motivo, l'oggetto C++/WinRT può chiamare la sua funzione membro protetta implements::get_strong per recuperare un riferimento forte al puntatore this. Si noti che non è necessario usare effettivamente la strong_this variabile nell'esempio di codice precedente. Chiamando semplicemente get_strong incrementa il conteggio dei riferimenti dell'oggetto C++/WinRT e mantiene valido questo puntatore implicito.

Importante

Poiché get_strong è una funzione membro del modello di struct winrt::implements , è possibile chiamarla solo da una classe che deriva direttamente o indirettamente da winrt::implements, ad esempio una classe C++/WinRT. Per altre info sulla derivazione da winrt::implements ed esempi, vedi Creare API con C++/WinRT.

Questo risolve il problema riscontrato in precedenza al passaggio 4. Anche se tutti gli altri riferimenti all'istanza della classe vengono meno, la coroutine si è premurata di garantire che le proprie dipendenze siano stabili.

Se un riferimento forte non è appropriato, allora è possibile chiamare implements::get_weak per ottenere un riferimento debole a questo. Basta confermare che è possibile recuperare un riferimento forte prima di accedere a questo. Anche get_weak è una funzione membro del template di 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"";
    }
}

Nell'esempio precedente, il riferimento debole non impedisce l'eliminazione definitiva dell'istanza della classe quando non rimangono riferimenti sicuri. Ma consente di controllare se è possibile acquisire un riferimento sicuro prima di accedere alla variabile membro.

Accedere in modo sicuro al puntatore this con un delegato per la gestione degli eventi

Scenario

Per informazioni generali sulla gestione degli eventi, vedi Gestire gli eventi usando delegati in C++/WinRT.

La sezione precedente ha evidenziato potenziali problemi legati al ciclo di vita nell'ambito delle coroutine e della concorrenza. Tuttavia, se si gestisce un evento con la funzione membro di un oggetto o dall'interno di una funzione lambda all'interno della funzione membro di un oggetto, è necessario considerare la durata relativa del destinatario dell'evento (l'oggetto che gestisce l'evento) e l'origine evento (l'oggetto che genera l'evento). Verranno ora esaminati alcuni esempi di codice.

Il listato di codice seguente definisce prima una classe EventSource semplice, che genera un evento generico gestito da tutti i delegati aggiunti. Questo esempio di evento utilizza il tipo di delegato Windows::Foundation::EventHandler, ma i problemi e le soluzioni qui descritti si applicano a qualsiasi tipo di delegato.

La classe EventRecipient fornisce quindi un gestore per l'evento EventSource::Event sotto forma di funzione 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();
}

Lo schema prevede che il destinatario dell'evento abbia un gestore di eventi lambda che dipende dal puntatore this. Ogni volta che il destinatario dell'evento sopravvive alla sorgente dell'evento, sopravvive anche a tali dipendenze. E in questi casi, che sono comuni, il modello funziona bene. Alcuni di questi casi sono evidenti, ad esempio quando una pagina dell'interfaccia utente gestisce un evento generato da un controllo presente nella pagina. La pagina ha una maggiore validità del pulsante, quindi il gestore sopravvive anche al pulsante. Ciò vale ogni volta che il destinatario possiede la sorgente (per esempio come membro dati) o ogni volta che il destinatario e la sorgente sono oggetti fratelli e sono entrambi posseduti direttamente da un altro oggetto.

Quando si è certi di trovarsi in un caso in cui l'handler non sopravviverà al this da cui dipende, è possibile acquisire this normalmente, senza dover considerare un ciclo di vita forte o debole.

Tuttavia, esistono ancora casi in cui this non rimane valido fino al momento del suo utilizzo in un gestore (inclusi i gestori per gli eventi di completamento e di avanzamento generati da azioni e operazioni asincrone), ed è importante sapere come gestirli.

  • Quando un'origine evento genera gli eventi in modo sincrono, è possibile revocare il gestore e assicurarsi di non ricevere altri eventi. Tuttavia, per gli eventi asincroni, anche dopo la revoca (e soprattutto quando la revoca avviene all'interno del distruttore), un evento già in corso potrebbe raggiungere l'oggetto dopo che ne è iniziata la distruzione. Trovare un luogo in cui annullare la sottoscrizione prima della distruzione potrebbe attenuare il problema, ma continuare a leggere per una soluzione affidabile.
  • Se si sta scrivendo una coroutine per implementare un metodo asincrono, allora è possibile.
  • In rari casi con determinati oggetti del framework dell'interfaccia utente XAML (SwapChainPanel, ad esempio), è possibile, se l'oggetto ricevente viene finalizzato senza essere stato annullato dalla registrazione dall'origine dell'evento.

Problema

Questa versione successiva della funzione principale simula ciò che accade quando il destinatario dell'evento viene eliminato definitivamente (forse esce dall'ambito) mentre l'origine evento sta ancora generando eventi.

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.
}

Il destinatario dell'evento viene eliminato definitivamente, ma il gestore eventi lambda al suo interno è ancora sottoscritto all'evento Event . Quando viene sollevato tale evento, la lambda tenta di dereferenziare il puntatore this, che in quel momento non è più valido. Pertanto, una violazione di accesso genera codice nel gestore (o nella continuazione di una coroutine) che tenta di usarlo.

Importante

Se si verifica una situazione simile a questa, sarà necessario considerare la durata dell'oggetto ; e se l'oggetto acquisito sopravvive o meno all'acquisizione. In caso contrario, acquisiscilo con un riferimento forte o debole, come illustrato di seguito.

Oppure, se ha senso nel tuo scenario e se i vincoli legati al threading lo consentono, un'altra opzione consiste nel revocare il gestore dell'evento dopo che il destinatario ha terminato di usarlo oppure nel distruttore del destinatario. Vedere Revocare un delegato registrato.

Questo è il modo in cui viene registrato il gestore.

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

L'espressione lambda acquisisce automaticamente tutte le variabili locali in base al riferimento. Quindi, per questo esempio, avremmo potuto scrivere in modo equivalente questo.

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

In entrambi i casi, stiamo semplicemente catturando il puntatore this grezzo. E questo non ha alcun effetto sul conteggio dei riferimenti, quindi nulla impedisce che l'oggetto corrente venga eliminato definitivamente.

La soluzione

La soluzione consiste nell'acquisire un riferimento sicuro (o, come si vedrà, un riferimento debole, se più appropriato). Un riferimento sicuro incrementa il conteggio dei riferimenti e mantiene attivo l'oggetto corrente. È sufficiente dichiarare una variabile di acquisizione (chiamata strong_this in questo esempio) e inizializzarla con una chiamata a implements::get_strong, che recupera un riferimento sicuro a questo puntatore.

Importante

Poiché get_strong è una funzione membro del modello di struct winrt::implements , è possibile chiamarla solo da una classe che deriva direttamente o indirettamente da winrt::implements, ad esempio una classe C++/WinRT. Per altre info sulla derivazione da winrt::implements ed esempi, vedi Creare API con C++/WinRT.

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

È anche possibile omettere la cattura automatica dell'oggetto corrente e accedere al membro dati tramite la variabile di cattura anziché tramite this.

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

Se un riferimento forte non è appropriato, allora è possibile chiamare implements::get_weak per ottenere un riferimento debole a questo. Un riferimento debole non mantiene attivo l'oggetto corrente. Quindi, basta confermare che sia ancora possibile recuperare un riferimento forte dal riferimento debole prima di accedere ai membri.

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;
    }
});

Se si acquisisce un puntatore non elaborato, sarà necessario assicurarsi di mantenere attivo l'oggetto a puntamento.

Se si utilizza una funzione membro come delegato

Oltre alle funzioni lambda, questi principi si applicano anche all'uso di una funzione membro come delegato. La sintassi è diversa, quindi si esaminerà il codice. Per prima cosa, ecco il gestore dell'evento della funzione membro potenzialmente non sicuro, che usa il puntatore raw 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;
    }
};

Questo è il modo standard e convenzionale per fare riferimento a un oggetto e alla relativa funzione membro. Per renderlo sicuro, è possibile, a partire dalla versione 10.0.17763.0 (Windows 10, versione 1809) dell'SDK di Windows, stabilire un riferimento sicuro o debole nel punto in cui è registrato il gestore. A questo punto, l'oggetto destinatario dell'evento è ancora attivo.

Per un riferimento forte, è sufficiente chiamare get_strong al posto del puntatore grezzo this. C++/WinRT garantisce che il delegato risultante contenga un riferimento sicuro all'oggetto corrente.

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

L'acquisizione di un riferimento sicuro significa che l'oggetto diventerà idoneo per la distruzione solo dopo che il gestore è stato annullato e tutti i callback in sospeso sono stati restituiti. Tuttavia, tale garanzia è valida solo nel momento in cui l'evento viene segnalato. Se il gestore dell'evento è asincrono, dovrai fornire alla coroutine un riferimento forte all'istanza della classe prima del primo punto di sospensione (per dettagli e codice, vedi la sezione Accesso sicuro al puntatore this in una coroutine membro della classe riportata in precedenza in questo argomento). Tuttavia, questo crea un riferimento circolare tra l'origine evento e l'oggetto, quindi è necessario interrompere in modo esplicito tale operazione revocando l'evento.

Per ottenere un riferimento debole, chiamare get_weak. C++/WinRT garantisce che il delegato risultante mantenga un riferimento debole. All'ultimo minuto e dietro le quinte, il delegato tenta di convertire il riferimento debole in un riferimento forte e chiama la funzione membro solo se l'operazione riesce.

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

Se il delegato chiama la funzione membro, C++/WinRT manterrà attivo l'oggetto fino alla restituzione del gestore. Tuttavia, se il tuo gestore è asincrono, restituisce il controllo nei punti di sospensione, quindi dovrai fornire alla tua coroutine un riferimento forte all'istanza della classe prima del primo punto di sospensione. Anche in questo caso, per altre info, vedi Accesso sicuro a questo puntatore in una sezione coroutine membro della classe più indietro in questo argomento.

Se la funzione membro non appartiene a un tipo di Windows Runtime

Quando il metodo get_strong non è disponibile (il tipo non è un tipo Windows Runtime), è possibile usare la tecnica illustrata nell'esempio di codice seguente. In questo caso viene visualizzata una normale classe C++ (denominata ConsoleNetworkWatcher) che gestisce l'evento 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;
};

Esempio di riferimento debole con SwapChainPanel::CompositionScaleChanged

In questo esempio di codice viene usato l'evento SwapChainPanel::CompositionScaleChanged tramite un'altra illustrazione di riferimenti deboli. Il codice registra un gestore di eventi tramite un'espressione lambda che cattura un riferimento debole al destinatario.

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.
}

Nella clausola di cattura lambda, viene creata una variabile temporanea che rappresenta un riferimento debole a this. Nel corpo della lambda, se è possibile ottenere un riferimento forte a this, viene chiamata la funzione OnCompositionScaleChanged. In questo modo, all'interno di OnCompositionScaleChanged, questo può essere usato in modo sicuro.

Riferimenti deboli in C++/WinRT

Qui sopra, abbiamo visto utilizzare riferimenti deboli. In generale, sono buoni per interrompere i riferimenti ciclici. Ad esempio, per l'implementazione nativa del framework dell'interfaccia utente basato su XAML, a causa della progettazione cronologica del framework, il meccanismo di riferimento debole in C++/WinRT è necessario per gestire riferimenti ciclici. Al di fuori di XAML, tuttavia, probabilmente non dovrai usare riferimenti deboli (non c'è niente di specifico di XAML su di essi). Piuttosto dovresti, più spesso di non, essere in grado di progettare le tue API C++/WinRT in modo da evitare la necessità di riferimenti ciclici e riferimenti deboli.

Per qualsiasi tipo che dichiari, non è subito chiaro a C++/WinRT se e quando sono necessari riferimenti deboli. C++/WinRT fornisce quindi automaticamente il supporto di riferimenti deboli nel modello di struct winrt::implements, da cui derivano direttamente o indirettamente i tipi C++/WinRT. È un meccanismo in cui paghi solo se lo usi, nel senso che non comporta alcun costo a meno che non venga effettivamente richiesta l'interfaccia IWeakReferenceSource per l'oggetto. È anche possibile scegliere esplicitamente di rifiutare esplicitamente tale supporto.

Esempi di codice

Il modello di struct winrt::weak_ref è un'opzione per ottenere un riferimento debole a un'istanza di classe.

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

In alternativa, è possibile usare la funzione di supporto winrt::make_weak.

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

La creazione di un riferimento debole non influisce sul conteggio dei riferimenti sull'oggetto stesso; causa solo l'allocazione di un blocco di controllo. Tale blocco di controllo si occupa dell'implementazione della semantica di riferimento debole. È quindi possibile provare a alzare di livello il riferimento debole a un riferimento sicuro e, se ha esito positivo, usarlo.

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

A condizione che esista ancora un altro riferimento sicuro, la chiamata weak_ref::get incrementa il conteggio dei riferimenti e restituisce il riferimento sicuro al chiamante.

Rifiuto esplicito del supporto di riferimento debole

Il supporto dei riferimenti deboli è automatico. È tuttavia possibile scegliere esplicitamente di rinunciare a tale supporto passando la struttura marcatore winrt::no_weak_ref come argomento del template alla classe di base.

Se si deriva direttamente da winrt::implements.

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

Se si sta creando una classe di runtime.

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

Non importa in quale punto del pacchetto di parametri variadico compaia la struttura marcatore. Se si richiede un riferimento debole per un tipo per cui è stata disattivata esplicitamente l'opzione, il compilatore mostrerà "This is only for weak ref support".

API importanti