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 familia winrt::make de asistentes para hacerlo. Este tema profundiza en una característica de C++/WinRT 2.0 que ayuda a diagnosticar el error de asignar directamente en la pila un objeto de tipo de implementación.
Este tipo de errores puede convertirse en bloqueos misteriosos o corrupciones que son difíciles de depurar y cuya depuración lleva mucho tiempo. Por lo tanto, esta es una característica importante y vale la pena comprender el fondo.
Establecimiento 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 (desde dentro de la implementación) que espera un IStringable como argumento.
void Print(IStringable const& stringable)
{
printf("%ls\n", stringable.ToString().c_str());
}
El problema es que nuestro tipo MyStringableno es un IStringable.
- Nuestro tipo MyStringable es una implementación de la interfaz IStringable .
- El tipo IStringable es un tipo proyectado.
Important
Es importante comprender la distinción entre un tipo de implementación y un tipo proyectado. Para conocer los conceptos y términos esenciales, asegúrese de leer Consumir API con C++/WinRT y Crear API 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, es necesario obtener una referencia para que los operadores de conversión se puedan usar 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 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 causarte problemas. Considere esta función auxiliar no útil.
IStringable MakeStringable()
{
return MyStringable(); // Incorrect.
}
O incluso simplemente esta afirmación aparentemente inofensiva.
IStringable stringable{ MyStringable() }; // Also incorrect.
Desafortunadamente, un código así sí se compilaba con C++/WinRT 1.0, a causa de esa conversión implícita. El problema (muy grave) es que podríamos estar devolviendo un tipo proyectado que apunta a un objeto gestionado por conteo de referencias cuya memoria subyacente está en la pila efímera.
Aquí hay algo más que se compiló con C++/WinRT 1.0.
MyStringable* stringable{ new MyStringable() }; // Very inadvisable.
Los punteros raw son una fuente peligrosa de errores y requieren mucho trabajo. No los use si no es necesario. C++/WinRT hace todo lo posible para que todo sea eficiente sin obligarte a usar punteros sin formato. Aquí hay otra cosa que se compiló 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. El Windows Runtime (y COM clásico antes) se basa en un recuento 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 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 de nuevo bastante cuestionable. La propiedad exclusiva se contrapone a la vida útil compartida del recuento intrínseco de referencias 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 olvidas hacerlo, te encontrarás con un error del compilador que alude a ello mediante una referencia a una función abstracta llamada use_make_function_to_create_this_object. No es exactamente un static_assert; pero está cerca. 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 sobrescritura para detectar la asignación directa, la plantilla de función winrt::make debe proporcionar de algún modo una sobrescritura del método virtual abstracto. Para ello, lo hace mediante una clase final derivada de la implementación 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 puede deducir posiblemente se producirá incluso si anteriormente eligió no marcar la clase de implementación como final. Así que eso es una mejora. El contrario es que la implementación no puede ser final. Una vez más, eso no tiene importancia porque el tipo instanciado siempre será final.
En tercer lugar, nada te impide marcar cualquier función virtual de tu implementación con 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 despacho virtual se limita a la interfaz binaria de la aplicación (ABI) (que siempre es final), y sus métodos de implementación se basan en polimorfismo estático, es decir, 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 que es muy positivo y da lugar a una expansión en línea 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 en las implementaciones COM clásicas porque, una vez más, todo era virtual, y era habitual trabajar directamente con punteros sin encapsular, por lo que resultaba fácil invocar accidentalmente delete en lugar de Release. C++/WinRT se esfuerza especialmente por poner difícil que trabajes directamente con punteros raw. Y tendrías que de verdad esforzarte mucho para obtener un puntero sin formato en C++/WinRT sobre el que potencialmente podrías llamar a delete. La semántica de valores implica que se trabaja con valores y referencias, y rara vez con 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 del Windows Runtime. No debería ser el código que escribes 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.