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.
Tal como se explica en Crear API con C++/WinRT, cuando crees un objeto de tipo de implementación, debes usar la familia de aplicaciones auxiliares winrt::make para hacerlo. En este tema se analiza en profundidad una característica de C++/WinRT 2.0 que te ayuda a diagnosticar el error que se produce al asignar directamente un objeto de tipo de implementación en la pila.
Estos errores pueden convertirse en bloqueos o daños misteriosos que son difíciles de depurar y requieren 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, necesitamos 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.
Lamentablemente, el código como ese sí que se ha compilado con C++/WinRT 1.0 debido a esa conversión implícita. El problema (muy grave) es que es posible que devolvamos un tipo proyectado que apunta a un objeto de recuento de referencias cuya memoria de respaldo 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 se las ha arreglado para que todo funcione de forma eficaz sin obligarte a usar punteros básicos. 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 ú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 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 invalidación para detectar la asignación directa, la plantilla de función winrt::make debe cumplir de algún modo la función virtual abstracta con una invalidación. Para hacerlo, se deriva de la implementación con una clase final que proporciona la invalidación. 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. Esto es muy bueno y produce una inserción 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 las ha arreglado para que te sea difícil tratar directamente con los punteros básicos. Y tendrías que esforzarte mucho para conseguir un puntero básico en C++/WinRT en el que pudieras 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.