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.
Como se explica en Author API con C++/WinRT, al crear un objeto de tipo de implementación, debe usar la winrt::make familia de asistentes para hacerlo. En este tema se profundiza en una característica de C++/WinRT 2.0 que le ayuda a diagnosticar el error de asignar directamente un objeto de tipo de implementación en la pila.
Estos errores pueden convertirse en misteriosos bloqueos o daños que son difíciles y lentos de depurar. Por lo tanto, esta es una característica importante y vale la pena comprender el fondo.
Configuración de la escena, con MyStringable
En primer lugar, consideremos una implementación sencilla de IStringable.
struct MyStringable : implements<MyStringable, IStringable>
{
winrt::hstring ToString() const { return L"MyStringable"; }
};
Ahora imagine que necesita llamar a una función (dentro de su implementación) que espera como argumento un IStringable.
void Print(IStringable const& stringable)
{
printf("%ls\n", stringable.ToString().c_str());
}
El problema es que nuestro tipo de myStringable
- Nuestro tipo de MyStringable es una implementación de la interfaz IStringable.
- El tipo IStringable es un tipo proyectado.
Importante
Es importante comprender la distinción entre un tipo de implementación y un tipo proyectado . Para conocer los conceptos y términos esenciales, no deje de leer Consumir APIs con C++/WinRT y Crear APIs con C++/WinRT.
El espacio entre una implementación y la proyección puede ser sutil para comprender. De hecho, para intentar que la implementación se sienta un poco más similar a la proyección, la implementación proporciona conversiones implícitas a cada uno de los tipos proyectados que implementa. Eso no significa que podamos hacer esto.
struct MyStringable : implements<MyStringable, IStringable>
{
winrt::hstring ToString() const;
void Call()
{
Print(this);
}
};
En su lugar, necesitamos obtener una referencia para que los operadores de conversión puedan utilizarse como candidatos para resolver la llamada.
void Call()
{
Print(*this);
}
Eso funciona. Una conversión implícita proporciona una conversión (muy eficaz) del tipo de implementación al tipo proyectado y es muy conveniente para muchos escenarios. Sin esa instalación, muchos tipos de implementación resultarían muy complicados de crear. Siempre que solo use la plantilla de función winrt::make (o la función winrt::make_self ) para asignar la implementación, todo está bien.
IStringable stringable{ winrt::make<MyStringable>() };
Posibles problemas con C++/WinRT 1.0
Sin embargo, las conversiones implícitas pueden meterte en problemas. Considere esta función auxiliar no útil.
IStringable MakeStringable()
{
return MyStringable(); // Incorrect.
}
O incluso solo esta declaración aparentemente inofensiva.
IStringable stringable{ MyStringable() }; // Also incorrect.
Desafortunadamente, el código como ese compilaba con C++/WinRT 1.0, debido a esa conversión implícita. El problema (muy grave) es que potencialmente estamos devolviendo un tipo proyectado que apunta a un objeto que cuenta con referencias y cuya memoria subyacente está en la pila efímera.
Esta es otra cosa que se compila con C++/WinRT 1.0.
MyStringable* stringable{ new MyStringable() }; // Very inadvisable.
Los punteros sin procesar son una fuente peligrosa y intensiva de errores. No los use si no es necesario. C++/WinRT hace un gran esfuerzo por hacer todo eficiente sin obligarte a usar punteros crudos. Esta es otra cosa que se compila con C++/WinRT 1.0.
auto stringable{ std::make_shared<MyStringable>(); } // Also very inadvisable.
Este es un error en varios niveles. Tenemos dos recuentos de referencia diferentes para el mismo objeto. Windows Runtime (y antes, el COM clásico) se basa en un conteo de referencias intrínseco que no es compatible con std::shared_ptr. std::shared_ptr tiene, por supuesto, muchas aplicaciones válidas; pero no es necesario cuando se comparten objetos de Windows Runtime (y COM clásico). Por último, esto también se compila con C++/WinRT 1.0.
auto stringable{ std::make_unique<MyStringable>() }; // Highly dubious.
Esto es nuevamente bastante cuestionable. La propiedad única está en oposición a la duración compartida del recuento de referencias intrínseco de MyStringable.
La solución con C++/WinRT 2.0
Con C++/WinRT 2.0, todos estos intentos de asignar directamente tipos de implementación provocan un error del compilador. Ese es el mejor tipo de error, y infinitamente mejor que un misterioso error en tiempo de ejecución.
Siempre que necesite realizar una implementación, simplemente puede usar winrt::make o winrt::make_self, como se muestra anteriormente. Y ahora, si se olvida de hacerlo, entonces recibirá un error del compilador que hace referencia a esto con una alusión a una función abstracta denominada use_make_function_to_create_this_object. No es exactamente un static_assert
, pero está cerca de serlo. Aun así, esta es la forma más confiable de detectar todos los errores descritos.
Esto significa que es necesario colocar algunas restricciones menores en la implementación. Dado que nos basamos en la ausencia de una sobreescritura para detectar la asignación directa, la plantilla de función winrt::make debe satisfacer de alguna manera la función virtual abstracta con una sobreescritura. Deriva de la implementación con una clase final
que proporciona la sobrescritura. Hay algunas cosas que observar sobre este proceso.
En primer lugar, la función virtual solo está presente en las compilaciones de depuración. Esto significa que la detección no afectará al tamaño de la tabla virtual en las compilaciones optimizadas.
En segundo lugar, dado que la clase derivada que winrt::make usa es final
, significa que cualquier desvirtualización que el optimizador pueda deducir se producirá incluso si anteriormente eligió no marcar la clase de implementación como final
. Así que eso es una mejora. El inverso es que tu implementación no puede ser final
. De nuevo, no tiene importancia porque el tipo instanciado siempre será final
.
En tercer lugar, nada impide que marques las funciones virtuales en tu implementación como final
. Por supuesto, C++/WinRT es muy diferente de com clásico e implementaciones como WRL, donde todo lo relacionado con la implementación tiende a ser virtual. En C++/WinRT, el envío virtual se limita a la interfaz binaria de la aplicación (ABI) (que siempre es final
), y los métodos de implementación se basan en el polimorfismo estático o en tiempo de compilación. Esto evita el polimorfismo en tiempo de ejecución innecesario y también significa que hay poca razón para las funciones virtuales en la implementación de C++/WinRT. Lo cual es muy bueno y conduce a un manejo mucho más predecible.
En cuarto lugar, dado que winrt::make inserta una clase derivada, la implementación no puede tener un destructor privado. Los destructores privados eran populares con implementaciones COM clásicas porque, de nuevo, todo era virtual y era común tratar directamente con punteros sin procesar y, por tanto, era fácil llamar accidentalmente a delete
en lugar de Release. C++/WinRT hace todo lo posible para que sea difícil tratar directamente con punteros crudos. Y tendrías que realmente salir de tu camino para obtener un puntero sin procesar en C++/WinRT en el que podrías llamar a delete
. La semántica de valores significa que se trata de valores y referencias; y rara vez de punteros.
Por lo tanto, C++/WinRT desafía nuestras nociones preconcebidas de lo que significa escribir código COM clásico. Y eso es perfectamente razonable porque WinRT no es COM clásico. COM clásico es el lenguaje de ensamblado de Windows Runtime. No debería ser el código que escribas todos los días. En su lugar, C++/WinRT le permite escribir código más parecido a C++moderno y mucho menos como COM clásico.