Referencias fuertes y débiles en C++/WinRT

Important

¿Construyendo con la SDK de Aplicaciones para Windows? El código de este artículo usa los espacios de nombres de UWP (Windows.UI.Xaml). Si tu proyecto está dirigido a WinUI 3 (SDK de Aplicaciones para Windows), sustituye Microsoft.UI.Xaml (y los espacios de nombres relacionados Microsoft.UI.*) en todo el texto. Consulta Correspondencia entre las API de UWP y el SDK de aplicaciones de Windows para ver la correspondencia completa y la guía de migración de la interfaz de usuario para consultar más detalles.

Windows Runtime es un sistema de recuento de referencias y, en un sistema así, es importante conocer la importancia y la diferencia entre las referencias fuertes y las débiles (y las referencias que no son ni una cosa ni la otra, como el puntero implícito this). Como verá en este tema, saber cómo administrar estas referencias correctamente puede significar la diferencia entre un sistema confiable que se ejecuta sin problemas y otro que se bloquea de forma impredecible. Al proporcionar funciones auxiliares con una integración profunda en la proyección de lenguaje, C++/WinRT le facilita parte del trabajo de desarrollar sistemas más complejos de manera sencilla y correcta.

Note

Con solo algunas excepciones, la compatibilidad con referencias débiles está activada de forma predeterminada para los tipos de Windows Runtime que consume o crea en C++/WinRT. Windows.UI.Composition y Windows.Devices.Input.PenDevice son ejemplos de excepciones; es decir, espacios de nombres en los que la compatibilidad con referencias débiles no está habilitada para esos tipos. Consulte también Si el delegado de revocación automática no logra registrarse.

Si vas a crear tipos, consulta la sección Referencias débiles en C++/WinRT en este tema.

Acceso seguro al puntero this en una corrutina miembro de clase

Para obtener más información sobre corrutinas y ejemplos de código, consulta Operaciones asincrónicas y simultaneidad con C++/WinRT.

La lista de código siguiente muestra un ejemplo típico de una corrutina que es una función miembro de una clase. Puede copiar y pegar este ejemplo en los archivos especificados en un nuevo proyecto de aplicación de consola de 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 pasa algún tiempo trabajando y, finalmente, devuelve una copia del MyClass::m_value miembro de datos. Llamar a RetrieveValueAsync hace que se cree un objeto asincrónico, y ese objeto tiene un puntero implícito a this (a través del cual, finalmente, se accede a m_value).

Recuerde que, en una corrutina, la ejecución es sincrónica hasta el primer punto de suspensión, donde se devuelve el control al autor de la llamada. En RetrieveValueAsync, el primero co_await es el primer punto de suspensión. Para cuando se reanude la corrutina (aproximadamente cinco segundos después, en este caso), podría haberle pasado cualquier cosa al puntero implícito this a través del cual accedemos a m_value.

Esta es la secuencia completa de eventos.

  1. En main, se crea una instancia de MyClass (myclass_instance).
  2. Se crea el objeto async, que apunta (mediante su this) a myclass_instance.
  3. La función winrt::Windows::Foundation::IAsyncAction::get alcanza su primer punto de suspensión, bloquea durante unos segundos y, a continuación, devuelve el resultado de RetrieveValueAsync.
  4. RetrieveValueAsync devuelve el valor de this->m_value.

El paso 4 solo es seguro siempre que esto siga siendo válido.

Pero, ¿qué ocurre si la instancia de clase se destruye antes de que se complete la operación asincrónica? Hay todo tipo de formas en que la instancia de clase podría salir del ámbito antes de que se haya completado el método asincrónico. Pero podemos simularlo estableciendo la instancia de clase en 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;
}

Después del punto en el que se destruye la instancia de clase, parece que no se hace referencia directamente a ella de nuevo. Pero, por supuesto, el objeto asíncrono tiene un puntero this a sí mismo e intenta usarlo para copiar el valor almacenado dentro de la instancia de la clase. La corrutina es una función miembro y espera poder usar su puntero con impunidad.

Con este cambio en el código, nos encontramos con un problema en el paso 4, porque la instancia de clase se ha destruido y esto ya no es válido. En cuanto el objeto asincrónico intenta acceder a la variable dentro de la instancia de clase, se bloqueará (o hará algo completamente indefinido).

La solución consiste en proporcionar la operación asincrónica (la corrutina), su propia referencia fuerte a la instancia de clase. Tal como está escrito actualmente, la corrutina mantiene de hecho un puntero raw this a la instancia de la clase; pero eso no basta para mantener con vida la instancia de la clase.

Para mantener activa la instancia de clase, cambie la implementación de RetrieveValueAsync a la que se muestra a continuación.

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

Una clase de C++/WinRT se deriva directa o indirectamente de la plantilla winrt::implements . Debido a eso, el objeto C++/WinRT puede llamar a su función miembro protegida implements::get_strong para obtener una referencia fuerte a su puntero this. Tenga en cuenta que no es necesario usar realmente la strong_this variable en el ejemplo de código anterior; simplemente llamar a get_strong incrementa el recuento de referencias del objeto C++/WinRT y mantiene este puntero implícito válido.

Important

Dado que get_strong es una función miembro de la plantilla de estructura winrt::implements , solo se puede llamar desde una clase que deriva directa o indirectamente de winrt::implements, como una clase de C++/WinRT. Para obtener más información sobre cómo derivar de winrt::implements y ejemplos, consulta Creación de API con C++/WinRT.

Esto resuelve el problema que teníamos anteriormente cuando llegamos al paso 4. Aunque todas las demás referencias a la instancia de clase desaparezcan, la corrutina ha tomado la precaución de garantizar que sus dependencias sean estables.

Si una referencia fuerte no es adecuada, puede llamar en su lugar a implements::get_weak para obtener una referencia débil a this. Solo tiene que confirmar que puede recuperar una referencia segura antes de acceder a esto. De nuevo, get_weak es una función miembro de la plantilla de estructura 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"";
    }
}

En el ejemplo anterior, la referencia débil no impide que la instancia de clase se destruya cuando no permanezcan referencias seguras. Sin embargo, proporciona una manera de comprobar si se puede adquirir una referencia segura antes de acceder a la variable miembro.

Acceso seguro al puntero this mediante un delegado de control de eventos

El escenario

Para obtener información general sobre el control de eventos, consulta Controlar eventos mediante delegados en C++/WinRT.

La sección anterior destacó posibles problemas relacionados con el tiempo de vida en los ámbitos de las corrutinas y la concurrencia. Pero, si maneja un evento mediante una función miembro de un objeto, o desde una función lambda dentro de la función miembro de un objeto, debe tener en cuenta los tiempos de vida relativos del receptor del evento (el objeto que maneja el evento) y del origen del evento (el objeto que genera el evento). Echemos un vistazo a algunos ejemplos de código.

La lista de código siguiente define primero una clase EventSource simple, que genera un evento genérico que controla cualquier delegado que se haya agregado a él. Este evento de ejemplo se produce para usar el tipo de delegado Windows::Foundation::EventHandler, pero los problemas y soluciones aquí se aplican a cualquier tipo de delegado y a todos los tipos de delegado.

A continuación, la clase EventRecipient proporciona un controlador para el evento EventSource::Event en forma de una función 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();
}

El patrón es que el receptor del evento tiene un controlador de eventos de tipo lambda con dependencias de su puntero this. Cada vez que el destinatario del evento sobrevive al origen del evento, sobrevivirá a esas dependencias. Y en esos casos, que son comunes, el patrón funciona bien. Algunos de estos casos son obvios, como cuando una página de interfaz de usuario controla un evento generado por un control que se encuentra en la página. La página sobrevive al botón, por lo que el controlador también sobrevive al botón. Esto es cierto siempre que el receptor posea el origen (por ejemplo, como miembro de datos), o siempre que el receptor y el origen sean hermanos y pertenezcan directamente a otro objeto.

Cuando esté seguro de encontrarse ante un caso en el que el manejador no tendrá una vida útil más larga que el this del que depende, puede capturar this normalmente, sin tener en cuenta si el tiempo de vida es fuerte o débil.

Pero todavía hay casos en los que this no sobrevive a su uso en un manejador (incluidos los manejadores de eventos de finalización y progreso generados por acciones y operaciones asíncronas), y es importante saber cómo tratarlos.

  • Cuando un origen de eventos genera sus eventos de forma sincrónica, puede revocar el controlador y estar seguro de que no recibirá más eventos. Pero, en el caso de eventos asíncronos, incluso después de revocar (y especialmente al revocar dentro del destructor), un evento en tránsito podría llegar a su objeto después de que haya comenzado a destruirse. Encontrar un lugar para cancelar la suscripción antes de la destrucción podría mitigar el problema, pero seguir leyendo para una solución sólida.
  • Si está creando una corrutina para implementar un método asíncrono, es posible.
  • En raras ocasiones, con determinados objetos del marco de interfaz de usuario XAML (SwapChainPanel, por ejemplo), esto es posible si el receptor se finaliza sin cancelar su registro en el origen del evento.

El problema

Esta siguiente versión de la función principal simula lo que sucede cuando se destruye el destinatario del evento (quizás sale del ámbito) mientras el origen del evento sigue generando 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.
}

El destinatario del evento se destruye, pero el controlador de eventos lambda dentro de él todavía está suscrito al evento Event . Cuando se genera ese evento, la expresión lambda intenta desreferenciar el puntero this, que en ese momento ya no es válido. Por lo tanto, una infracción de acceso resulta del código en el controlador (o en la continuación de una corrutina) que intenta usarlo.

Important

Si te encuentras en una situación como esta, tendrás que pensar en el tiempo de vida del objeto this y en si el objeto this capturado sigue existiendo más allá de la captura. Si no es así, captúrelo con una referencia fuerte o débil, como demostraremos a continuación.

O bien —si tiene sentido en su caso y si las consideraciones relativas a los subprocesos siquiera lo permiten—, otra opción es revocar el controlador después de que el destinatario haya terminado de procesar el evento, o bien en el destructor del destinatario. Consulte Revocar un delegado registrado.

Así es como registramos el controlador.

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

La expresión lambda captura automáticamente las variables locales por referencia. Por lo tanto, para este ejemplo, podríamos haber escrito esto de forma equivalente.

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

En ambos casos, solo capturamos el puntero this sin procesar. Y eso no tiene ningún efecto en el recuento de referencias, por lo que nada impide que el objeto actual se destruya.

La solución

La solución consiste en capturar una referencia segura (o, como veremos, una referencia débil si es más adecuada). Una referencia segura incrementa el recuento de referencias y mantiene activo el objeto actual. Simplemente declare una variable de captura (denominada strong_this en este ejemplo) e inicialícela con una llamada a implements::get_strong, que recupera una referencia fuerte a nuestro puntero this.

Important

Dado que get_strong es una función miembro de la plantilla de estructura winrt::implements , solo se puede llamar desde una clase que deriva directa o indirectamente de winrt::implements, como una clase de C++/WinRT. Para obtener más información sobre cómo derivar de winrt::implements y ejemplos, consulta Creación de API con C++/WinRT.

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

Incluso puede omitir la captura automática del objeto actual y acceder al miembro de datos mediante la variable de captura en lugar de hacerlo mediante el this implícito.

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

Si una referencia fuerte no es adecuada, puede llamar en su lugar a implements::get_weak para obtener una referencia débil a this. Una referencia débil no mantiene activo el objeto actual. Por lo tanto, simplemente confirme que todavía puede recuperar una referencia segura de la referencia débil antes de acceder a los miembros.

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

Si captura un puntero sin procesar, deberá asegurarse de mantener activo el objeto al que apunta.

Si utiliza una función miembro como delegado

Además de las funciones lambda, estos principios también se aplican cuando se utiliza una función miembro como delegado. La sintaxis es diferente, por lo que echemos un vistazo a algún código. Primero, este es el controlador de eventos de una función miembro potencialmente no seguro, que utiliza un puntero this sin procesar.

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 es la forma estándar y convencional de hacer referencia a un objeto y a su función miembro. Para que esto sea seguro, puede, a partir de la versión 10.0.17763.0 (Windows 10, versión 1809) del SDK de Windows, establecer una referencia fuerte o débil en el momento en que se registra el controlador. En ese momento, se sabe que el objeto del destinatario del evento sigue activo.

Para obtener una referencia fuerte, simplemente llame a get_strong en lugar del puntero this en bruto. C++/WinRT garantiza que el delegado resultante contenga una referencia segura al objeto actual.

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

Capturar una referencia fuerte significa que el objeto solo podrá destruirse después de que se haya cancelado el registro del manejador y de que todas las devoluciones de llamada pendientes hayan finalizado. Sin embargo, esa garantía solo es válida cuando se desencadena el evento. Si su controlador de eventos es asincrónico, tendrá que proporcionar a su corrutina una referencia fuerte a la instancia de la clase antes del primer punto de suspensión (para obtener más información y ver el código, consulte la sección Acceso seguro al puntero this en una corrutina miembro de una clase, mencionada anteriormente en este tema). Pero eso crea una referencia circular entre el origen del evento y el objeto, por lo que debe interrumpirlo explícitamente revocando el evento.

Para obtener una referencia débil, llame a get_weak. C++/WinRT garantiza que el delegado resultante contenga una referencia débil. En el último momento y entre bastidores, el delegado intenta convertir la referencia débil en una referencia fuerte y solo llama a la función miembro si lo consigue.

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

Si el delegado llama a la función miembro, C++/WinRT mantendrá activo el objeto hasta que el controlador devuelva. Sin embargo, si tu manejador es asíncrono, retornará en los puntos de suspensión y, por tanto, tendrás que proporcionar a tu corrutina una referencia fuerte a la instancia de la clase antes del primer punto de suspensión. De nuevo, para obtener más información, consulte la sección Acceso seguro al puntero this en una corrutina miembro de una clase anterior de este tema.

Si la función miembro no pertenece a un tipo de Windows Runtime

Cuando el método get_strong no está disponible para usted (el tipo no es un tipo Windows Runtime), puede usar la técnica que se muestra en el ejemplo de código siguiente. Aquí se muestra una clase C++ normal (denominada ConsoleNetworkWatcher) que controla el 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;
};

Ejemplo de referencia débil mediante SwapChainPanel::CompositionScaleChanged

En este ejemplo de código, usamos el evento SwapChainPanel::CompositionScaleChanged mediante otra ilustración de referencias débiles. El código registra un controlador de eventos mediante una expresión lambda que captura una referencia débil 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.
}

En la cláusula de captura de lamba, se crea una variable temporal, que representa una referencia débil a esto. En el cuerpo de la lambda, si se puede obtener una referencia fuerte a this, se llama a la función OnCompositionScaleChanged. De este modo, dentro de OnCompositionScaleChanged, se puede usar this de forma segura.

Referencias débiles en C++/WinRT

Anteriormente, vimos que se usaban referencias débiles. En general, son buenos para romper las referencias cíclicas. Por ejemplo, para la implementación nativa del marco de interfaz de usuario basado en XAML, debido al diseño histórico del marco, el mecanismo de referencia débil en C++/WinRT es necesario para controlar las referencias cíclicas. Sin embargo, fuera de XAML, probablemente no necesites usar referencias débiles (no porque tengan nada intrínsecamente específico de XAML). En su lugar, deberías, con más frecuencia que no, poder diseñar tus propias API de C++/WinRT de forma que evites la necesidad de referencias cíclicas y referencias débiles.

Para cualquier tipo que declares, C++/WinRT no sabe de inmediato si se necesitan referencias débiles y, de ser así, cuándo. Por lo tanto, C++/WinRT proporciona compatibilidad de referencia débil automáticamente en la plantilla de estructura winrt::implements, desde la que se derivan directamente o indirectamente sus propios tipos de C++/WinRT. Es de coste por uso, en el sentido de que no tiene ningún coste a menos que realmente se consulte el objeto para ver si implementa IWeakReferenceSource. Y puede optar explícitamente por no participar en ese soporte técnico.

Ejemplos de código

La plantilla de winrt::weak_ref struct es una opción para obtener una referencia débil a una instancia de una clase.

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

O bien, puede usar la función auxiliar winrt::make_weak.

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

La creación de una referencia débil no afecta al recuento de referencias en el propio objeto; simplemente hace que se asigne un bloque de control. Ese bloque de control se encarga de implementar la semántica de referencia débil. A continuación, puede intentar convertir la referencia débil en una referencia segura y, si lo consigue, usarla.

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

Siempre que todavía exista alguna otra referencia fuerte, la llamada weak_ref::get incrementa el recuento de referencias y devuelve la referencia fuerte al autor de la llamada.

Desactivación del soporte para referencias débiles

La compatibilidad con referencias débiles es automática. Pero puedes optar explícitamente por renunciar a esa compatibilidad pasando la estructura marcadora winrt::no_weak_ref como argumento de plantilla para tu clase base.

Si derivas directamente de winrt::implements.

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

Si va a crear una clase en tiempo de ejecución.

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

No importa dónde aparezca en el paquete de parámetros variádicos la estructura de marcador. Si solicita una referencia débil para un tipo excluido, el compilador le mostrará "Esto solo sirve para admitir referencias débiles".

API importantes