Nota:
El acceso a esta página requiere autorización. Puede intentar iniciar sesión o cambiar directorios.
El acceso a esta página requiere autorización. Puede intentar cambiar los directorios.
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 su proyecto está dirigido a WinUI 3 (SDK de aplicaciones para Windows), sustituya 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 de forma segura al puntero this en una corrutina de 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.
En la lista de código siguiente se 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 el control se devuelve al autor de la llamada. En RetrieveValueAsync, el primero co_await es el primer punto de suspensión. Para cuando se reanude la corrutina (unos cinco segundos más tarde, en este caso), podría haber ocurrido algo implícito con el objeto this del puntero que usamos para obtener acceso a m_value.
Esta es la secuencia completa de eventos.
- En main, se crea una instancia de MyClass (
myclass_instance). - Se crea el objeto
async, que apunta (mediante su this) amyclass_instance. - 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.
-
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 asincrónico tiene un puntero this a ella e intenta usarlo para copiar el valor almacenado dentro de la instancia de clase. La corrutina es una función miembro, y espera que pueda usar su puntero this 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 a la operación asincrónica (esto es, la corrutina) su propia referencia fuerte a la instancia de clase. Como está escrito actualmente, la corrutina contiene efectivamente un puntero this básico a la instancia de clase; pero eso no es suficiente para mantener activa la instancia de 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 . Por eso, el objeto C++/WinRT puede llamar a su función miembro protegida implements::get_strong para recuperar una referencia fuerte al 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. Incluso si todas las demás referencias a la instancia de clase desaparecen, 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.
En la sección anterior se resaltan los posibles problemas de duración de las áreas de corrutinas y simultaneidad. 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, sobrevive 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 lo sobrevive. 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és seguro de que tienes un caso en el que el controlador no sobrevivirá al objeto this del que depende, puedes capturar this de forma normal, sin tener en cuenta una duración segura o no segura.
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 sincrónicamente, puedes revocar el controlador y estar seguro de que no recibirás más eventos. Pero para los eventos asincrónicos, incluso después de la revocación (y especialmente al revocar dentro del destructor), un evento en curso podría alcanzar el objeto después de que se haya iniciado la destrucción. 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 vas a crear una corrutina para implementar un método asincrónico, entonces es posible.
- En raras ocasiones con ciertos objetos del marco de la interfaz de usuario XAML (SwapChainPanel, por ejemplo) es posible, siempre que se haya finalizado el destinatario sin anular el registro del 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, se produce una infracción de acceso desde el código en el controlador (o en la continuación de una corrutina) que intenta utilizarlo.
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úralo con una referencia fuerte o débil, como te mostramos a continuación.
O bien (si es que tiene sentido para su escenario y si las consideraciones de subprocesos lo hacen posible), otra opción es revocar el controlador una vez hecho el destinatario con el evento o en el destructor del destinatario. Consulte Revocar un delegado registrado.
Así es cómo 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 tanto, solo tienes que confirmar que todavía puedes recuperar una referencia fuerte desde 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 capturas un puntero sin formato, tienes que asegurarte de mantener activo el objeto al que apuntas.
Si usas una función miembro como delegado
Además de las funciones lambda, estos principios también se aplican al uso de una función miembro como tu delegado. La sintaxis es diferente, por lo que echemos un vistazo a algún código. En primer lugar, este es el controlador de eventos de la función miembro potencialmente inseguro, que utiliza un puntero this básico.
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, solo tienes que llamar a get_strong en lugar al puntero this básico. C++/WinRT garantiza que el delegado resultante contenga una referencia segura al objeto actual.
event_source.Event({ get_strong(), &EventRecipient::OnEvent });
La captura de una referencia fuerte significa que el objeto será pasible de ser destruido solo después de que se haya anulado el registro del controlador y se hayan devuelto todas las devoluciones de llamada pendientes. Sin embargo, esa garantía solo es válida en el momento en que se genera el evento. Si el controlador de eventos es asincrónico, tendrás que dar a la corrutina una referencia fuerte a la instancia de clase antes del primer punto de suspensión (para más detalles y el código, consulta la sección Acceso de forma segura al puntero this en una corrutina de miembro de clase 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 en segundo plano, el delegado intenta resolver la referencia débil a una fuerte y solo llama a la función miembro si es correcto.
event_source.Event({ get_weak(), &EventRecipient::OnEvent });
Si el delegado llama a la función miembro, C++/WinRT mantendrá el objeto activo hasta que vuelva el controlador. Sin embargo, si el controlador es asincrónico, se devuelve en los puntos de suspensión, por lo que tendrás que dar a la corrutina una referencia fuerte a la instancia de clase antes del primer punto de suspensión. De nuevo, para más información, consulta la sección Acceso de forma segura al puntero this en una corrutina de miembro de clase anteriormente en 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 del lambda, si puede obtenerse 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 un sistema de pago pay-to-play, es decir, no te cuesta nada a menos que en realidad se consulte tu objeto para 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.
Optar por no recibir el soporte de referencia débil
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 vas 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".