Silné a slabé odkazy v jazyce C++/WinRT

Důležité

Vyvíjíte pomocí sady Windows App SDK? Kód tohoto článku používá obory názvů UPW (Windows.UI.Xaml). Pokud váš projekt cílí na WinUI 3 (Windows App SDK), nahraďte Microsoft.UI.Xaml (a související Microsoft.UI.* obory názvů) po celou dobu. Úplný přehled mapování najdete v tématu Mapování rozhraní API UWP na Windows App SDK a další podrobnosti v průvodci migrací uživatelského rozhraní.

prostředí Windows Runtime je systém založený na počítání referencí; a v takovém systému je důležité znát význam silných a slabých referencí i rozdíl mezi nimi (a také referencí, které nejsou ani jedním z nich, například implicitního ukazatele this). Jak v této kapitole uvidíte, vědět, jak tyto reference správně spravovat, může znamenat rozdíl mezi spolehlivým systémem, který funguje bez problémů, a systémem, který nečekaně padá. Díky tomu, že nabízí pomocné funkce, které jsou hluboce podporovány jazykovou projekcí, vám C++/WinRT usnadňuje vytváření složitějších systémů jednoduše a správně.

Note

S několika výjimkami je podpora slabých odkazů ve výchozím nastavení zapnutá pro typy prostředí Windows Runtime, které používáte nebo které vytvoříte v jazyce C++/WinRT. Windows.UI.Composition a Windows.Devices.Input.PenDevice jsou příklady výjimek – tedy oblasti názvů, ve kterých pro tyto typy není povolena podpora slabých odkazů. Viz také Pokud se delegáta pro automatické odvolání nepodaří zaregistrovat.

Pokud vytváříte typy, přečtěte si část Slabé reference v C++/WinRT v tomto tématu.

Bezpečný přístup k tomuto ukazateli v korutině člena třídy

Další informace o rutinách a ukázky kódu najdete v tématu Souběžnost a asynchronní operace s C++/WinRT.

Následující výpis kódu ukazuje typický příklad korutiny, která je členskou funkcí třídy. Tento příklad můžete zkopírovat do zadaných souborů v novém projektu konzolové aplikace 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 stráví nějakou dobu prací a nakonec vrátí kopii datového členu MyClass::m_value . Volání RetrieveValueAsync způsobí vytvoření asynchronního objektu a tento objekt má implicitní tento ukazatel (prostřednictvím kterého je nakonec m_value přístupný).

Nezapomeňte, že v korutině je provádění synchronní až do prvního bodu pozastavení, kde se ovládací prvek vrátí volajícímu. V RetrieveValueAsync je první co_await bod pozastavení. V době, kdy se korutina znovu spustí (v tomto případě přibližně o pět sekund později), se s implicitním ukazatelem this, přes který přistupujeme k m_value, mohlo stát cokoli.

Tady je úplná posloupnost událostí.

  1. V hlavním případě se vytvoří instance MyClass (myclass_instance).
  2. Objekt async je vytvořen, odkazující (prostřednictvím tohoto) na myclass_instance.
  3. Winrt::Windows::Foundation::IAsyncAction::get funkce dosáhne prvního bodu pozastavení, blokuje několik sekund a vrátí výsledek RetrieveValueAsync.
  4. Funkce RetrieveValueAsync vrátí hodnotu this->m_value.

Krok 4 je bezpečný pouze za předpokladu , že tento krok zůstane platný.

Ale co když je instance třídy zničena před dokončením asynchronní operace? Existuje mnoho způsobů, jak se instance třídy může přestat nacházet v rozsahu platnosti ještě před dokončením asynchronní metody. Můžeme ho ale simulovat nastavením instance třídy na 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;
}

Od okamžiku, kdy zrušíme instanci třídy, to vypadá, že už na ni přímo neodkazujeme. Samozřejmě, že asynchronní objekt má tento ukazatel na něj a snaží se ji použít ke zkopírování hodnoty uložené uvnitř instance třídy. Korutina je členská funkce a očekává, že bude moci bez omezení používat svůj ukazatel this.

Při této změně kódu narazíme na problém v kroku 4, protože instance třídy byla zničena, a to už není platné. Jakmile se asynchronní objekt pokusí přistoupit k proměnné uvnitř instance třídy, zhroutí se (nebo provede něco zcela nedefinovaného).

Řešením je zajistit, aby asynchronní operace – korutina – měla vlastní silnou referenci na instanci třídy. V současné podobě korutina v podstatě drží surový ukazatel this na instanci třídy, ale to nestačí k udržení instance třídy naživu.

Pokud chcete zachovat instanci třídy naživu, změňte implementaci RetrieveValueAsync na hodnotu uvedenou níže.

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

Třída C++/WinRT přímo nebo nepřímo pochází z winrt::implements šablony . Z tohoto důvodu může objekt C++/WinRT volat svou implements::get_strong chráněnou členskou funkci, aby získal silný odkaz na svůj ukazatel this. Všimněte si, že ve výše uvedeném příkladu kódu není nutné proměnnou strong_this skutečně používat; samotné volání get_strong zvýší počet odkazů objektu C++/WinRT a zachová platnost jeho implicitního ukazatele this.

Důležité

Protože get_strong je členskou funkcí šablony struktury winrt::implements, můžete ji volat pouze z třídy, která přímo nebo nepřímo dědí z winrt::implements, například z třídy C++/WinRT. Další informace o odvození z winrt::implements a příkladů najdete v tématu Vytváření rozhraní API pomocí C++/WinRT.

Tím se vyřeší problém, který jsme měli dříve, když jsme se dostali ke kroku 4. I když veškeré ostatní odkazy na instanci třídy zmizí, korutina se předem postarala o to, aby její závislosti zůstaly stabilní.

Pokud silná reference není vhodná, můžete místo toho zavolat implements::get_weak a získat slabou referenci na this. Jen se ujistěte, že můžete získat silnou referenci před přístupem k this. get_weak je opět členskou funkcí šablony struktury 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"";
    }
}

V předchozím příkladu slabý odkaz neudržuje instanci třídy před zničením, pokud nezůstanou žádné silné odkazy. Poskytuje ale možnost ověřit, zda lze před přístupem k členské proměnné získat silnou referenci.

Bezpečný přístup k tomuto ukazateli pomocí delegáta zpracování událostí

Scénář

Obecné informace o zpracování událostí najdete v tématu Zpracování událostí pomocí delegátů v C++/WinRT.

Předchozí část poukázala na potenciální problémy týkající se životnosti v oblasti korutin a souběžnosti. Pokud ale zpracováváte událost pomocí členské funkce objektu nebo z funkce lambda uvnitř členské funkce objektu, musíte se zamyslet nad relativními životnostmi příjemce události (objekt zpracovávající událost) a zdrojem události (objektem, který událost vyvolává). Podívejme se na některé příklady kódu.

Následující výpis kódu nejprve definuje jednoduchou třídu EventSource , která vyvolá obecnou událost, která je zpracována všemi delegáty, kteří do ní byli přidáni. Tato ukázková událost používá typ delegáta Windows::Foundation::EventHandler, ale zde popsané problémy a jejich řešení platí pro všechny typy delegátů.

Pak EventRecipient třída poskytuje obslužnou rutinu pro EventSource::Event událost ve formě lambda funkce.

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

Vzor je, že příjemce události má obslužnou rutinu události lambda se závislostmi na tomto ukazateli. Pokud příjemce události přežije zdroj události, přežije i tyto závislosti. A v takových případech, které jsou běžné, vzor funguje dobře. Některé z těchto případů jsou zřejmé, například když stránka uživatelského rozhraní zpracovává událost vyvolanou ovládacím prvku na stránce. Stránka existuje déle než tlačítko — takže i obslužná funkce existuje déle než tlačítko. To platí pokaždé, když příjemce vlastní zdroj (například jako datový člen) nebo když příjemce a zdroj jsou sourozenecké objekty a oba jsou přímo vlastněny jiným objektem.

Pokud jste si jistí, že jde o případ, kdy obslužná funkce nepřežije this, na kterém závisí, můžete pak this zachytit běžným způsobem, aniž byste museli brát v úvahu silnou nebo slabou správu životnosti.

Ale stále existují případy, kdy this nepřežije své použití v obslužné rutině (včetně obslužných rutin událostí dokončení a průběhu vyvolaných asynchronními akcemi a operacemi), a je důležité vědět, jak tyto případy řešit.

  • Když zdroj událostí vyvolá události synchronně, můžete odvolat obslužnou rutinu a mít jistotu, že nebudete dostávat žádné další události. Ale u asynchronních událostí, a to i po odvolání (a zejména při odvolání uvnitř destruktoru), může událost v letu dosáhnout objektu po zahájení destrukce. Nalezení místa pro odhlášení odběru před zničením může tento problém zmírnit, ale pokračujte ve čtení pro robustní řešení.
  • Pokud vytvoříte korutinu pro implementaci asynchronní metody, je to možné.
  • Ve výjimečných případech s určitými objekty architektury uživatelského rozhraní XAML (SwapChainPanel), je to možné, pokud je příjemce finalizován bez zrušení registrace ze zdroje události.

Problém

Tato další verze hlavní funkce simuluje, co se stane, když je příjemce události zničen (možná dojde mimo rozsah), zatímco zdroj událostí stále vyvolává události.

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

Příjemce události je zničen, ale obslužná rutina lambda pro událost v něm je stále přihlášená k události Event. Když je tato událost vyvolána, lambda se pokusí dereferencovat ukazatel this, který je v tu chvíli neplatný. K porušení přístupu tedy dochází v důsledku toho, že se ho kód v obslužné rutině (nebo v pokračování korutiny) pokouší použít.

Důležité

Pokud narazíte na takovou situaci, budete muset přemýšlet o životnosti tohoto objektu; a zda zachycený objekt prožije zachycení. Pokud ne, zachyťte ho silnou nebo slabou referencí, jak si ukážeme níže.

Nebo – pokud to dává smysl pro váš scénář a pokud je to s ohledem na vlákna vůbec možné – další možností je odregistrovat obslužnou rutinu poté, co příjemce událost zpracuje, nebo v destruktoru příjemce. Viz Odvolání registrovaného delegáta.

Takto registrujeme obslužnou funkci.

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

Lambda automaticky zachytává všechny lokální proměnné odkazem. V tomto příkladu bychom to mohli napsat odpovídajícím způsobem.

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

V obou případech jen zachycujeme nezpracovaný ukazatel . A to nemá žádný vliv na počítání odkazů, takže nic nebrání tomu, aby byl aktuální objekt zničen.

Řešení

Řešením je zachytit silný odkaz (nebo jak uvidíme, slabý odkaz, pokud je to vhodnější). Silný odkaz zvýší počet odkazů a zachová aktuální objekt naživu. Stačí deklarovat zachytávací proměnnou (v tomto příkladu nazývanou strong_this) a inicializovat ji voláním implements::get_strong, které získá silnou referenci na náš ukazatel this.

Důležité

Protože get_strong je členskou funkcí šablony struktury winrt::implements, můžete ji volat pouze z třídy, která přímo nebo nepřímo dědí z winrt::implements, například z třídy C++/WinRT. Další informace o odvození z winrt::implements a příkladů najdete v tématu Vytváření rozhraní API pomocí C++/WinRT.

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

Můžete dokonce vynechat automatické zachycení aktuálního objektu a přistupovat k datovému členu prostřednictvím zachycené proměnné namísto implicitního this.

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

Pokud silná reference není vhodná, můžete místo toho zavolat implements::get_weak a získat slabou referenci na this. Slabý odkaz neudržuje aktuální objekt naživu. Takže před přístupem k členům stačí ověřit, že ze slabé reference stále můžete získat silnou referenci.

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

Pokud zachytíte surový ukazatel, musíte zajistit, aby objekt, na který ukazuje, zůstal existovat.

Používáte-li členovou funkci jako delegáta

Kromě funkcí lambda se tyto principy vztahují také na použití členské funkce jako delegáta. Syntaxe se liší, takže se podíváme na nějaký kód. Nejprve je zde potenciálně nebezpečná členská obslužná funkce události, která používá surový ukazatel 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;
    }
};

Toto je standardní, konvenční způsob, jak odkazovat na objekt a jeho členovou funkci. Pokud chcete tuto bezpečnost zabezpečit, můžete od verze 10.0.17763.0 (Windows 10, verze 1809) sady Windows SDK vytvořit silný nebo slabý odkaz v okamžiku, kdy je obslužná rutina registrována. V tomto okamžiku je známo, že objekt příjemce události stále existuje.

Pro silnou referenci jednoduše zavolejte get_strong místo surového ukazatele this. C++/WinRT zajišťuje, že výsledný delegát bude obsahovat silný odkaz na aktuální objekt.

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

Zachycení silné reference znamená, že objekt bude možné zničit až poté, co bude zrušena registrace obslužné funkce a všechna dosud probíhající zpětná volání budou dokončena. Tato záruka je však platná pouze v době, kdy je událost vyvolána. Pokud je obslužná funkce události asynchronní, budete muset své korutině před prvním bodem pozastavení zajistit silný odkaz na instanci třídy (podrobnosti a ukázku kódu najdete v části Bezpečný přístup k ukazateli this v korutině členské funkce třídy v předchozí části tohoto tématu). Tím se ale vytvoří cyklický odkaz mezi zdrojem událostí a objektem, takže ho musíte explicitně přerušit odvoláním události.

U slabého odkazu volejte get_weak. C++/WinRT zajistí, že výsledný delegát bude obsahovat slabý odkaz. Na poslední chvíli a v zákulisí se delegát pokusí převést slabou referenci na silnou a členskou funkci zavolá pouze tehdy, pokud se to podaří.

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

Pokud delegát skutečně zavolá vaši členskou funkci, pak C++/WinRT udrží váš objekt naživu, dokud se vaše obslužná funkce nevrátí. Pokud je však obslužná rutina asynchronní, vrátí se v bodech pozastavení a proto budete muset korutině poskytnout silný odkaz na instanci třídy před prvním bodem pozastavení. Další informace najdete výše v tomto tématu v části Bezpečný přístup k ukazateli this v korutině člena třídy.

Pokud členová funkce nepatří do typu prostředí Windows Runtime

Pokud nemáte k dispozici metodu get_strong (váš typ není typem prostředí Windows Runtime), můžete použít techniku znázorněnou v níže uvedeném příkladu kódu. Zde je zobrazena běžná třída C++ (s názvem ConsoleNetworkWatcher), která zpracovává událost 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;
};

Slabý příklad odkazu s využitím SwapChainPanel::CompositionScaleChanged

V tomto ukázkovém kódu používáme událost SwapChainPanel::CompositionScaleChanged jako další ukázku použití slabých referencí. Kód zaregistruje obslužnou rutinu události pomocí lambda, která zachycuje slabý odkaz na příjemce.

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

V klauzuli lamba capture se vytvoří dočasná proměnná, která představuje slabý odkaz na tuto hodnotu. V těle výrazu lambda, pokud lze získat silnou referenci na this, je zavolána funkce OnCompositionScaleChanged. Tímto způsobem lze uvnitř OnCompositionScaleChanged bezpečně použít this.

Slabé odkazy v C++/WinRT

Výše jsme viděli, že se používají slabé odkazy. Obecně jsou vhodné k přerušení cyklických referencí. Například pro nativní implementaci architektury uživatelského rozhraní založeného na XAML – kvůli historickému návrhu rozhraní – je pro zpracování cyklických odkazů nezbytný slabý referenční mechanismus v jazyce C++/WinRT. Mimo oblast XAML však pravděpodobně nebudete potřebovat používat slabé reference (ne že by na nich bylo něco, co je samo o sobě specifické pro XAML). Místo toho byste měli být schopni navrhnout vlastní rozhraní API C++/WinRT tak, aby se zabránilo nutnosti cyklických odkazů a slabých odkazů.

Pro každý typ, který deklarujete, není pro jazyk C++/WinRT zřejmé, jestli nebo když jsou potřeba slabé odkazy. C++/WinRT tedy automaticky poskytuje podporu slabých odkazů prostřednictvím šablony struktury winrt::implements, od níž se vaše vlastní typy C++/WinRT přímo či nepřímo odvozují. Jde o model „platíte jen za to, co používáte“, protože vás to nic nestojí, dokud u vašeho objektu není skutečně vyžádáno rozhraní IWeakReferenceSource. A můžete se výslovně rozhodnout, že se z této podpory odhlásíte.

Příklady kódu

Šablona struktury winrt::weak_ref je jednou z možností získání slabého odkazu na instanci třídy.

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

Nebo můžete použít pomocnou funkci winrt::make_weak.

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

Vytvoření slabého odkazu nemá vliv na počet odkazů na samotný objekt; pouze způsobí přidělení řídicího bloku. Tento řídicí blok se postará o implementaci slabé sémantiky odkazů. Potom se můžete pokusit zvýšit úroveň slabého odkazu na silný odkaz a v případě úspěchu ho použít.

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

Za předpokladu, že stále existuje nějaký jiný silný odkaz, weak_ref::get zvýší počet odkazů a vrátí silný odkaz volajícímu.

Výslovný nesouhlas se slabými referenčními podporami

Podpora slabých referencí je automatická. Můžete se ale této podpory explicitně vzdát tím, že své základní třídě předáte značkovou strukturu winrt::no_weak_ref jako argument šablony.

Pokud odvozujete přímo z winrt::implements.

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

Pokud vytváříte třídu runtime.

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

Nezáleží na tom, kde se ve balíčku variadických parametrů objeví značková struktura. Pokud požádáte o slabou referenci pro typ, který tuto možnost nepodporuje, kompilátor vypíše hlášení "Toto je pouze pro podporu slabých referencí".

Důležitá rozhraní API