Riferimenti sicuri e deboli in C++/WinRT

Windows Runtime è un sistema con conteggio dei riferimenti e in un sistema di questo tipo è importante conoscere il significato e la differenza tra riferimenti sicuri e riferimenti deboli, così come i riferimenti di nessuno di questi tipi, come il puntatore this implicito. Come si vedrà in questo argomento, sapere come gestire correttamente questi riferimenti può fare la differenza tra un sistema affidabile che viene eseguito senza problemi e uno con arresti anomali imprevedibili. Offrendo funzioni helper con supporto avanzato nella proiezione del linguaggio, C++/WinRT ti viene incontro per il lavoro di creazione di sistemi più complessi in modo semplice e corretto.

Nota

Con solo poche eccezioni, il supporto dei riferimenti deboli è attivo 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, ovvero spazi dei nomi in cui il supporto dei riferimenti deboli non è attivo per tali tipi. Vedere anche Se la registrazione del delegato di revoca automatica ha esito negativo.

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

Accesso sicuro al puntatore this in una coroutine membro di classe

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

Il listato di codice seguente mostra un esempio tipico di una coroutine che è una funzione membro di una classe. Puoi copiare e 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 esegue alcune elaborazioni e alla fine restituisce una copia del membro dati MyClass::m_value. La chiamata di RetrieveValueAsync causa la creazione di un oggetto asincrono e tale oggetto ha un puntatore this implicito, tramite il quale avviene alla fine l'accesso a m_value.

Tieni presente che in una coroutine l'esecuzione è sincrona fino al primo punto di sospensione, dove il controllo viene restituito al chiamante. In RetrieveValueAsync, la prima occorrenza di co_await è il primo punto di sospensione. Entro il tempo in cui la coroutine riprende l'esecuzione (in questo caso cinque minuti dopo), può essere accaduta qualsiasi cosa al puntatore this implicito tramite il quale si accede a m_value.

Ecco la sequenza di eventi completa.

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

Il passaggio 4 è sicuro purché this rimanga valido.

Ma cosa accade se l'istanza della classe viene eliminata definitivamente prima del completamento dell'operazione asincrona? Sono tantissimi i modi in cui l'istanza della classe può uscire dall'ambito prima del completamento del metodo asincrono, ma possiamo fare una simulazione 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 viene eliminata l'istanza della classe, sembra che non ci siano altri riferimenti diretti. Ma l'oggetto asincrono ha ovviamente un puntatore this a tale classe e tenta di usarlo per copiare il valore archiviato all'interno dell'istanza della classe. Le coroutine è una funzione membro e si aspetta di poter usare il relativo puntatore this impunemente.

Con questa modifica al codice, si verifica un problema nel passaggio 4, perché l'istanza della classe è stata eliminata definitivamente e il puntatore this non è più valido. Non appena l'oggetto asincrono tenta di accedere alla variabile all'interno dell'istanza della classe, subirà un arresto anomalo (o eseguirà operazioni del tutto indefinite).

La soluzione consiste nell'assegnare all'operazione asincrona, la coroutine, un riferimento sicuro proprio all'istanza della classe. Così come è scritta, la coroutine mantiene in modo efficace un puntatore this non elaborato all'istanza della classe, ma ciò non è sufficiente per mantenere attiva l'istanza della classe.

Per mantenere attiva l'istanza della classe, modifica l'implementazione di RetrieveValueAsync come indicato 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 funzione membro protetta implements::get_strong corrispondente per recuperare un riferimento sicuro al puntatore this. Tieni presente che non è effettivamente necessario usare la variabile strong_this nell'esempio di codice precedente. È sufficiente chiamare get_strong per incrementare il conteggio dei riferimenti dell'oggetto C++/WinRT per mantenere valido il relativo puntatore this implicito.

Importante

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

Viene così risolto il problema che si è presentato in precedenza in corrispondenza del passaggio 4. Anche se scompaiono tutti gli altri riferimenti all'istanza della classe, la coroutine ha preso la precauzione di garantire che le relative dipendenze siano stabili.

Se un riferimento sicuro non è appropriato, puoi chiamare invece implements::get_weak per recuperare un riferimento debole a this. Conferma semplicemente che è possibile recuperare un riferimento sicuro prima di accedere a this. Anche in questo caso get_weak è una funzione membro del modello 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 dell'istanza della classe quando non rimangono riferimenti sicuri, ma offre un modo per controllare se è possibile acquisire un riferimento sicuro prima dell'accesso alla variabile membro.

Accesso sicuro al puntatore this con un delegato di gestione degli eventi

Scenario

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

Nella sezione precedente sono stati evidenziati i potenziali problemi di durata a livello di coroutine e concorrenza. Ma se gestisci un evento con una funzione membro di un oggetto o dall'interno di una funzione lambda all'interno della funzione membro di un oggetto, devi considerare le durate relative del destinatario dell'evento (l'oggetto che gestisce l'evento) e dell'origine dell'evento (l'oggetto che genera l'evento). Esaminiamo alcuni esempi di codice.

Il listato di codice riportato di seguito definisce prima di tutto una semplice classe EventSource, che genera un evento generico che viene gestito da qualsiasi delegato aggiunto. Questo evento di esempio usa il tipo di delegato Windows::Foundation::EventHandler, ma i problemi e le soluzioni qui descritti si applicano a tutti i tipi di delegato.

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

Il modello prevede che il destinatario di eventi disponga di un gestore di eventi lambda con dipendenze dal relativo puntatore this. Ogni volta che il destinatario di eventi ha una durata superiore a quella dell'origine eventi, ha anche una durata superiore a tali dipendenze e in questi casi, comuni, il modello funziona bene. Alcuni di questi casi sono ovvi, come quando una pagina dell'interfaccia utente gestisce un evento generato da un controllo presente su tale pagina. La pagina ha una durata superiore al pulsante, quindi anche il gestore ha una durata superiore al pulsante. Questo è vero ogni volta che il destinatario è il proprietario dell'origine (ad esempio come membro dati) oppure ogni volta che il destinatario e l'origine sono elementi di pari livello appartenenti direttamente a un altro oggetto.

Quando sei certo di avere un caso in cui il gestore non avrà una durata superiore all'oggetto this da cui dipende, puoi acquisire this normalmente, senza prendere in considerazione la durata sicura o debole.

Tuttavia, esistono casi in cui la durata di this non andrà oltre il suo utilizzo in un gestore, inclusi i gestori per gli eventi di avanzamento e completamento generati da operazioni e azioni asincrone, ed è importante sapere come gestire questi casi.

  • Quando un'origine genera gli eventi in modo sincrono, puoi revocare il gestore e avere la certezza che non riceverai altri eventi. Per gli eventi asincroni, tuttavia, anche dopo la revoca (e in particolare quando la revoca viene eseguita all'interno del distruttore), è possibile che un evento in corso raggiunga l'oggetto dopo l'avvio del processo di distruzione. L'identificazione di una posizione in cui annullare la sottoscrizione prima della distruzione può attenuare il problema, ma continua la lettura di questo articolo per informazioni su una soluzione solida e affidabile.
  • Se crei una coroutine per implementare un metodo asincrono, allora è possibile.
  • In rari casi, con alcuni oggetti del framework dell'interfaccia utente XAML (ad esempio SwapChainPanel), è possibile, se il destinatario viene finalizzato senza l'annullamento della registrazione dell'origine evento.

Problema

La prossima versione della funzione main simula cosa accade quando viene eliminato definitivamente il destinatario di eventi (ad esempio quando esce dall'ambito) mentre l'origine eventi 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 di eventi viene eliminato, ma il gestore di eventi lambda al suo interno è ancora incluso nella sottoscrizione dell'evento Event. Quando viene generato tale evento, la funzione lambda tenta di dereferenziare il puntatore this, che a questo punto non è valido. Ne risulta quindi una violazione di accesso dal codice nel gestore, o nella continuazione di una coroutine, che tenta di usarlo.

Importante

Se si verifica una situazione di questo tipo, dovrai prendere in considerazione la durata dell'oggetto this e la possibilità che l'oggetto this acquisito abbia o meno una durata superiore all'acquisizione. In caso contrario, esegui l'acquisizione con un riferimento sicuro o debole, come verrà dimostrato di seguito.

Se opportuno per il tuo scenario e se le considerazioni sul threading lo rendono possibile, un'altra opzione consiste nel revocare il gestore dopo che il destinatario avrà finito con l'evento o nel distruttore del destinatario. Vedi Revocare un delegato registrato.

Ecco come si registra il gestore.

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

La funzione lambda acquisisce automaticamente tutte le variabili locali per riferimento. Per questo esempio, quindi, si potrebbe scrivere questo codice equivalente.

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

In entrambi i casi, si sta semplicemente acquisendo il puntatore this non elaborato. Ciò non ha alcun effetto sul conteggio dei riferimenti, quindi niente impedisce l'eliminazione definitiva dell'oggetto corrente.

La soluzione

La soluzione consiste nell'acquisire un riferimento sicuro oppure, 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 al puntatore this.

Importante

Dato che get_strong è una funzione membro del modello di struct winrt::implements, puoi chiamarla solo da una classe che deriva direttamente o indirettamente da winrt::implements, ad esempio una classe C++/WinRT. Per altre informazioni 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;
});

Puoi anche omettere l'acquisizione automatica dell'oggetto corrente e accedere al membro dati tramite la variabile di acquisizione anziché tramite il puntatore implicito this.

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

Se un riferimento sicuro non è appropriato, puoi chiamare invece implements::get_weak per recuperare un riferimento debole a this. Un riferimento debole non mantiene attivo l'oggetto corrente. Verifica quindi che sia comunque possibile recuperare un riferimento sicuro da quest'ultimo 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 acquisisci un puntatore non elaborato, dovrai assicurarti che l'oggetto a cui punta venga mantenuto attivo.

Se usi una funzione membro come delegato

Oltre che alle funzioni lambda, questi principi si applicano anche all'uso di una funzione membro come delegato. La sintassi è diversa, quindi verrà presentato un esempio di codice. Prima di tutto ecco il gestore eventi della funzione membro potenzialmente non sicuro che usa un puntatore this non elaborato.

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, a partire dalla versione 10.0.17763.0 (Windows 10, versione 1809) di Windows SDK puoi stabilire un riferimento sicuro o debole al momento della registrazione del gestore. A questo punto, è noto che l'oggetto destinatario di eventi è ancora attivo.

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

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

Se viene acquisito un riferimento sicuro, l'oggetto diventa idoneo per la distruzione solo dopo che è stata annullata la registrazione del gestore e sono stati restituiti tutti i callback in attesa. Questa garanzia è tuttavia valida solo nel momento in cui viene generato l'evento. Se il gestore eventi è asincrono, dovrai assegnare alla coroutine un riferimento sicuro all'istanza della classe prima del primo punto di sospensione. Per informazioni dettagliate e codice, vedi la sezione precedente Accesso sicuro al puntatore this in una coroutine membro di classe in questo argomento. Questa operazione ha tuttavia l'effetto di creare un riferimento circolare tra l'origine dell'evento e l'oggetto, che è quindi necessario interrompere in modo esplicito revocando l'evento.

Per un riferimento debole, chiama get_weak. C++/WinRT garantisce che il delegato risultante mantenga un riferimento debole. All'ultimo minuto, e dietro le quinte, il delegato tenta di risolvere il riferimento debole in uno sicuro e chiama la funzione membro solo se riesce.

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

Se il delegato chiama la funzione membro, C++/WinRT mantiene attivo l'oggetto fino a quando il gestore non restituisce il controllo. Se tuttavia il gestore è asincrono, il controllo viene restituito in corrispondenza dei punti di sospensione e dovrai quindi assegnare alla coroutine un riferimento sicuro all'istanza della classe prima del primo punto di sospensione. Anche in questo caso, per altre informazioni, vedi la sezione precedente Accesso sicuro al puntatore this in una coroutine membro di classe in questo argomento.

Se la funzione membro non appartiene a un tipo Windows Runtime

Quando il metodo get_strong non è disponibile (il vostro tipo non è un tipo Windows Runtime), è possibile utilizzare la tecnica mostrata nell'esempio di codice seguente. Qui viene mostrata una normale classe (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 per continuare con la presentazione dei riferimenti deboli. Il codice registra un gestore eventi usando una funzione lambda che acquisisce un riferimento debole al destinatario.

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

void RegisterEventHandler()
{
    m_compositionScaleChangedEventToken = m_swapChainPanel.CompositionScaleChanged([weak_this{ get_weak() }]
        (Windows::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(Windows::UI::Xaml::Controls::SwapChainPanel const& sender,
    Windows::Foundation::IInspectable const& object)
{
    // Here, we know that the "this" object is valid.
}

Nella clausola di acquisizione lamba viene creata una variabile temporanea, che rappresenta un riferimento debole a this. Nel corpo della funzione lambda, se è possibile ottenere un riferimento sicuro a this, viene chiamata la funzione OnCompositionScaleChanged. In questo modo, in OnCompositionScaleChanged è possibile usare this in modo sicuro.

Riferimenti deboli in C++/WinRT

In precedenza è stato illustrato l'uso dei riferimenti deboli. In generale, sono utili per interrompere i riferimenti ciclici. Ad esempio, per quanto riguarda l'implementazione nativa del framework dell'interfaccia utente basata su XAML, a causa della progettazione pregressa di tale framework il meccanismo del riferimento debole in C++/WinRT è necessario per gestire i riferimenti ciclici. Al di fuori di XAML, tuttavia, probabilmente non avrai la necessità di usare riferimenti deboli, anche se in effetti non hanno alcuna caratteristica intrinsecamente specifica di XAML. Dovresti spesso essere in grado di progettare le tue API C++/WinRT in modo da evitare la necessità di riferimenti ciclici e deboli.

Per ogni tipo di dato che dichiari, non è immediatamente evidente per C++/WinRT se o quando sono necessari i riferimenti deboli. Pertanto, C++/WinRT offre automaticamente il supporto dei riferimenti deboli nel modello di struct winrt::implements, da cui derivano direttamente o indirettamente i tuoi tipi C++/WinRT. È di tipo "pay-for-play", ovvero non ha alcun costo a meno che non venga effettivamente eseguita la query all'oggetto per IWeakReferenceSource. E puoi scegliere 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, puoi usare la funzione helper winrt::make_weak.

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

La creazione di un riferimento debole non influenza il conteggio dei riferimenti per l'oggetto stesso, ma causa semplicemente l'allocazione di un blocco di controllo. Il blocco di controllo si occupa dell'implementazione della semantica di riferimento debole. Puoi quindi provare a promuovere il riferimento debole in 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 di weak_ref::get incrementa il conteggio dei riferimenti e restituisce il riferimento sicuro al chiamante.

Rifiuto esplicito del supporto di riferimenti deboli

Il supporto di riferimenti deboli è automatico. Puoi però scegliere di rifiutare esplicitamente tale supporto passando lo struct indicatore winrt::no_weak_ref come argomento del modello per la classe base.

Se la classe 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>
{
    ...
}

La posizione in cui appare l'elemento struct dell'indicatore nel pacchetto di parametri variadic non ha importanza. Se richiedi un riferimento debole per un tipo rifiutato esplicitamente, il compilatore ti aiuterà visualizzando il messaggio "This is only for weak ref support" (Questo è solo per il supporto di riferimento debole).

API importanti