Referências fortes e fracas em C++/WinRT

Important

Construir com o SDK de Aplicações Windows? O código deste artigo utiliza espaços de nomes UWP(Windows.UI.Xaml). Se o seu projeto tem como alvo o WinUI 3 (SDK de Aplicações Windows), substitua Microsoft.UI.Xaml (e namespaces relacionadosMicrosoft.UI.*) ao longo de todo o processo. Consulte Mapeamento das APIs UWP para o SDK de Aplicações Windows para obter um mapeamento completo e guia de migração da interface do utilizador para obter detalhes adicionais.

O Windows Runtime é um sistema de contagem de referências; e, num sistema desse tipo, é importante conhecer a importância e a distinção entre referências fortes e fracas (e referências que não são nenhuma delas, como o ponteiro implícito this). Como verá neste tópico, saber gerir estas referências corretamente pode fazer a diferença entre um sistema fiável que funciona de forma fluida e um que crasha de forma imprevisível. Ao fornecer funções auxiliares que têm um suporte profundo na projeção da linguagem, C++/WinRT encontra-te a meio caminho no teu trabalho de construir sistemas mais complexos de forma simples e correta.

Note

Com poucas exceções, o suporte para referências fracas está ativado por predefinição para os tipos do Windows Runtime que utiliza ou cria em C++/WinRT. Windows.UI.Composition e Windows.Devices.Input.PenDevice são exemplos de exceções — ou seja, espaços de nomes em que o suporte para referências fracas não está ativado para esses tipos. Veja também Se o seu delegado para revogação automática não se registar.

Se estiveres a criar tipos, vê a secção Referências fracas em C++/WinRT neste tópico.

Aceder em segurança ao ponteiro this numa corrotina de membro da classe

Para mais informações sobre corrotinas e exemplos de código, veja Concorrência e operações assíncronas com C++/WinRT.

A listagem de código abaixo mostra um exemplo típico de uma corrotina que é uma função-membro de uma classe. Pode copiar e colar este exemplo nos ficheiros especificados numa nova aplicação Windows Console (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 demora algum tempo a trabalhar e, eventualmente, devolve uma cópia do MyClass::m_value membro de dados. Chamar RetrieveValueAsync faz com que um objeto assíncrono seja criado, e esse objeto tem um ponteiro implícito (através do qual, eventualmente, m_value é acedido).

Lembre-se de que, numa corrotina, a execução é síncrona até ao primeiro ponto de suspensão, momento em que o controlo é devolvido ao chamador. No RetrieveValueAsync, o primeiro co_await é o primeiro ponto de suspensão. Quando a corrotina é retomada (cerca de cinco segundos depois, neste caso), muita coisa pode ter acontecido ao ponteiro implícito this através do qual acedemos a m_value.

Aqui está a sequência completa dos acontecimentos.

  1. Principalmente, cria-se uma instância de MyClass (myclass_instance).
  2. O objeto async é criado, ficando a apontar (através de this) para myclass_instance.
  3. A função winrt::Windows::Foundation::IAsyncAction::get atinge o seu primeiro ponto de suspensão, bloqueia durante alguns segundos e depois devolve o resultado de RetrieveValueAsync.
  4. RetrieveValueAsync devolve o valor de this->m_value.

O Passo 4 é seguro apenas enquanto este permanecer válido.

Mas e se a instância de classe for destruída antes da operação assíncrona terminar? Existem várias formas de a instância de classe sair do âmbito antes de o método assíncrono estar concluído. Mas podemos simulá-lo definindo a instância da classe para 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;
}

Depois do momento em que destruímos a instância de classe, parece que não voltamos a referir-nos diretamente a ela. Mas, claro, o objeto assíncrono tem um ponteiro this para ela e tenta usá-lo para copiar o valor armazenado na instância da classe. A corrotina é uma função-membro e espera poder usar o seu ponteiro this com impunidade.

Com esta alteração no código, deparamo-nos com um problema no passo 4, porque a instância da classe foi destruída e isso já não é válido. Assim que o objeto assíncrono tenta aceder à variável dentro da instância de classe, ele crasha (ou faz algo completamente indefinido).

A solução é dar à operação assíncrona — a corrotina — uma referência forte própria à instância da classe. Tal como está escrita atualmente, a corrotina mantém efetivamente um ponteiro this em bruto para a instância da classe; mas isso não é suficiente para manter a instância da classe viva.

Para manter a instância da classe ativa, altere a implementação do RetrieveValueAsync para a mostrada abaixo.

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

Uma classe C++/WinRT deriva direta ou indiretamente do modelo winrt::implements . Por causa disso, o objeto C++/WinRT pode chamar a sua implements::get_strong função-membro protegida para obter uma referência forte ao ponteiro this. Note que não há necessidade de usar a strong_this variável no exemplo de código acima; simplesmente chamar get_strong incrementa a contagem de referências do objeto C++/WinRT e mantém este ponteiro implícito válido.

Important

Como get_strong é uma função-membro do modelo de estrutura winrt::implements, só o pode chamar numa classe que derive direta ou indiretamente de winrt::implements, como uma classe C++/WinRT. Para obter mais informações sobre como derivar de winrt::implements e exemplos, consulte Criar APIs com C++/WinRT.

Isto resolve o problema que tínhamos quando chegámos ao passo 4. Mesmo que todas as outras referências à instância da classe desapareçam, a corrotina tomou a precaução de garantir que as suas dependências são estáveis.

Se uma referência forte não for apropriada, pode, em vez disso, chamar implements::get_weak para obter uma referência fraca a this. Basta confirmar que consegues obter uma referência forte antes de aceder a isto. Mais uma vez, get_weak é uma função-membro do modelo de estrutura 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"";
    }
}

No exemplo acima, a referência fraca não impede que a instância da classe seja destruída quando não restam referências fortes. Mas dá-lhe uma forma de verificar se uma referência forte pode ser adquirida antes de aceder à variável membro.

Aceder em segurança ao ponteiro this com um delegado para processamento de eventos

O cenário

Para informações gerais sobre gestão de eventos, veja Gerir eventos usando delegados em C++/WinRT.

A secção anterior destacou potenciais problemas ao longo da vida nas áreas de corrotinas e concorrência. Mas, se lidares com um evento com a função membro de um objeto, ou dentro de uma função lambda dentro da função membro de um objeto, então tens de pensar nas vidas relativas do destinatário do evento (o objeto que trata do evento) e da fonte do evento (o objeto que gera o evento). Vamos ver alguns exemplos de código.

A lista de código abaixo define primeiro uma classe simples EventSource , que gera um evento genérico que é tratado por quaisquer delegados que lhe tenham sido adicionados. Este evento de exemplo utiliza o tipo de delegado Windows::Foundation::EventHandler, mas os problemas e soluções aqui aplicam-se a todos os tipos de delegado.

Depois, a classe EventRecipient fornece um handler para o evento EventSource::Event sob a forma de uma função 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();
}

O padrão é que o recetor do evento possui um manipulador de eventos do tipo lambda com dependências do respetivo ponteiro this. Sempre que o destinatário do evento sobrevive à fonte do evento, sobrevive a essas dependências. E nesses casos, que são comuns, o padrão funciona bem. Alguns destes casos são óbvios, como quando uma página de interface gere um evento gerado por um controlo que está na página. A página sobrevive ao botão — por isso, o manipulador também sobrevive ao botão. Isto é válido sempre que o destinatário possui a fonte (como membro dos dados, por exemplo), ou sempre que o destinatário e a fonte são irmãos e pertencem diretamente a outro objeto.

Quando tiveres a certeza de que tens um caso em que o manipulador não vai sobreviver ao this do qual depende, então podes capturar this normalmente, sem ter em conta o tempo de vida forte ou fraco.

Mas ainda há casos em que isto não ultrapassa a sua utilização num handler (incluindo handlers para eventos de conclusão e progresso gerados por ações e operações assíncronas), e é importante saber como lidar com eles.

  • Quando uma fonte de eventos eleva os seus eventos de forma síncrona, pode revogar o seu handler e ter a certeza de que não receberá mais eventos. Mas para eventos assíncronos, mesmo após a revogação (e especialmente ao revogar dentro do destruidor), um evento em voo pode chegar ao seu objeto depois de este ter começado a destruir. Encontrar um local para cancelar a subscrição antes da destruição pode mitigar o problema, mas continue a ler para uma solução robusta.
  • Se estiveres a escrever uma corrotina para implementar um método assíncrono, é possível.
  • Em casos raros com determinados objetos de estruturas de IU XAML (SwapChainPanel, por exemplo), isso é possível se o objeto recetor for finalizado sem anular o registo na origem do evento.

A questão

Esta próxima versão da função principal simula o que acontece quando o destinatário do evento é destruído (talvez saia do âmbito) enquanto a fonte do evento ainda está a gerar eventos.

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

O destinatário do evento é destruído, mas o manipulador de eventos lambda dentro dele continua registado no evento Event. Quando esse evento é acionado, a expressão lambda tenta desreferenciar o ponteiro this, que nessa altura já é inválido. Assim, uma violação de acesso resulta do código no handler (ou na continuação de uma corrotina) que tenta usá-lo.

Important

Se se deparar com uma situação destas, terá de pensar na vida útil deste objeto; e se o objeto capturado sobrevive ou não à captura. Se não o fizer, então capte-o com uma referência forte ou fraca, como vamos demonstrar abaixo.

Ou — se fizer sentido para o teu cenário, e se considerações de threading o tornarem sequer possível — outra opção é revogar o handler depois de o destinatário terminar o evento, ou no destruidor do destinatário. Ver Revogar um delegado registado.

É assim que estamos a registar o handler.

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

A lambda captura automaticamente quaisquer variáveis locais por referência. Portanto, para este exemplo, poderíamos ter escrito isto de forma equivalente.

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

Em ambos os casos, estamos apenas a capturar o ponteiro this bruto. E isso não tem efeito na contagem de referências, por isso nada impede que o objeto atual seja destruído.

A solução

A solução é captar uma referência forte (ou, como veremos, uma referência fraca, se for mais apropriado). Uma referência forte incrementa o número de referências e mantém o objeto atual vivo. Basta declarar uma variável de captura (chamada strong_this neste exemplo) e inicializá-la com uma chamada para implements::get_strong, que recupera uma referência forte para este ponteiro.

Important

Como get_strong é uma função-membro do modelo de estrutura winrt::implements, só o pode chamar numa classe que derive direta ou indiretamente de winrt::implements, como uma classe C++/WinRT. Para obter mais informações sobre como derivar de winrt::implements e exemplos, consulte Criar APIs com C++/WinRT.

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

Pode até omitir a captura automática do objeto atual e aceder ao membro de dados através da variável de captura, em vez de o fazer através do this implícito.

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

Se uma referência forte não for apropriada, pode, em vez disso, chamar implements::get_weak para obter uma referência fraca a this. Uma referência fraca não mantém o objeto atual vivo. Assim, basta confirmar que ainda consegue obter uma referência forte a partir da referência fraca antes de aceder aos membros.

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 capturares um apontador bruto, terás de garantir que mantém o objeto apontado vivo.

Se usar uma função de membro como delegado

Para além das funções lambda, estes princípios também se aplicam ao uso de uma função membro como seu delegado. A sintaxe é diferente, por isso vamos olhar para algum código. Primeiro, aqui está o manipulador de eventos de função-membro potencialmente inseguro, que utiliza um ponteiro bruto 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;
    }
};

Esta é a forma padrão e convencional de se referir a um objeto e à sua função elementar. Para tornar isto seguro, pode—a partir da versão 10.0.17763.0 (Windows 10, versão 1809) do Windows SDK—estabelecer uma referência forte ou fraca no ponto onde o handler está registado. Nesse momento, sabe-se que o objeto destinatário do evento ainda está vivo.

Para uma referência forte, basta chamar get_strong em vez do ponteiro this em bruto. C++/WinRT garante que o delegado resultante tem uma forte referência ao objeto atual.

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

Capturar uma referência forte significa que o seu objeto só se tornará elegível para destruição depois de o manipulador ter sido desregistado e todas as chamadas pendentes terem regressado. No entanto, essa garantia só é válida no momento em que o evento é gerado. Se o seu manipulador de eventos for assíncrono, terá de atribuir à sua corrotina uma referência forte à instância da classe antes do primeiro ponto de suspensão (para mais detalhes e para o código, consulte a secção Aceder em segurança ao ponteiro this numa corrotina membro de uma classe, anteriormente neste tópico). Mas isso cria uma referência circular entre a fonte do evento e o teu objeto, por isso tens de quebrar explicitamente isso revogando o teu evento.

Para obter uma referência fraca, chame get_weak. C++/WinRT assegura que o delegado resultante tem uma referência fraca. No último minuto, e nos bastidores, o delegado tenta resolver a referência fraca a um elemento forte, e só chama a função do membro se for bem-sucedida.

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

Se o delegado chamar a tua função membro, então o C++/WinRT manterá o teu objeto ativo até o teu handler voltar. No entanto, se o teu handler for assíncrono, então ele regressa aos pontos de suspensão, por isso terás de dar à tua corutina uma referência forte à instância da classe antes do primeiro ponto de suspensão. Mais uma vez, para mais informações, veja Acesso Seguro a este ponteiro numa secção de corutinas de membros da classe anteriormente neste tópico.

Se a função-membro não pertencer a um tipo do Windows Runtime

Quando o método get_strong não está disponível para si (o seu tipo não é um tipo Windows Runtime), pode usar a técnica mostrada no exemplo de código abaixo. Aqui, uma classe C++ normal (chamada ConsoleNetworkWatcher) é mostrada a tratar do 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;
};

Um exemplo de referência fraca que utiliza SwapChainPanel::CompositionScaleChanged

Neste exemplo de código, usamos o evento SwapChainPanel::CompositionScaleChanged como outra ilustração de referências fracas. O código regista um gestor de eventos usando um lambda que captura uma referência fraca ao destinatário.

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

Na cláusula de captura de lambda, é criada uma variável temporária, que representa uma referência fraca a this. No corpo da expressão lambda, se for possível obter uma referência forte a this, então a função OnCompositionScaleChanged é chamada. Assim, dentro do OnCompositionScaleChanged, isto pode ser usado em segurança.

Referências fracas em C++/WinRT

Anteriormente, vimos o uso de referências fracas. Em geral, são boas para quebrar referências cíclicas. Por exemplo, para a implementação nativa do framework de UI baseado em XAML — devido ao design histórico do framework — o mecanismo de referência fraco em C++/WinRT é necessário para lidar com referências cíclicas. Fora do XAML, no entanto, provavelmente não vais precisar de usar referências fracas (não que haja nada inerentemente específico de XAML nelas). Na verdade, deveria, na maioria das vezes, conseguir desenhar as suas próprias APIs C++/WinRT de forma a evitar a necessidade de referências cíclicas e referências fracas.

Para qualquer tipo que declares, não é imediatamente óbvio para C++/WinRT se ou quando são necessárias referências fracas. Assim, o C++/WinRT fornece automaticamente suporte para referências fracas no modelo de estrutura winrt::implements, da qual os seus próprios tipos C++/WinRT derivam direta ou indiretamente. É pay-for-play, ou seja, não te custa nada a menos que o teu objeto seja realmente consultado para o IWeakReferenceSource. E pode optar explicitamente por não receber esse suporte.

Exemplos de código

O modelo de struct winrt::weak_ref é uma opção para obter uma referência fraca a uma instância de classe.

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

Ou pode usar a função auxiliar winrt::make_weak.

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

Criar uma referência fraca não afeta a contagem de referências no próprio objeto; Só faz com que um bloco de controlo seja alocado. Esse bloco de controlo assegura a implementação da semântica das referências fracas. Podes então tentar promover a referência fraca para uma referência forte e, se tiveres sucesso, usá-la.

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

Desde que ainda exista alguma outra referência forte, a chamada weak_ref::get incrementa a contagem de referências e devolve a referência forte ao interlocutor.

Desativar o suporte para referências fracas

Suporte de referência fraco é automático. Mas podes optar explicitamente por prescindir desse suporte, passando a estrutura marcadora winrt::no_weak_ref como argumento de modelo para a tua classe base.

Se derivar diretamente de winrt::implements.

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

Se estiveres a criar uma classe de runtime.

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

Não importa onde no pacote de parâmetros variádicos aparece a estrutura de marcadores. Se pedires uma referência fraca para um tipo que não aderiu, o compilador apresenta a mensagem "Isto serve apenas para suporte a referências fracas".

APIs importantes