Diagnosticando alocações diretas

Conforme explicado nas APIs de Autor com C++/WinRT, ao criar um objeto do tipo de implementação, você deve usar a família winrt::make de auxiliares para fazer isso. Este tópico aborda em profundidade uma funcionalidade do C++/WinRT 2.0 que ajuda você a diagnosticar o erro de alocar diretamente na pilha um objeto do tipo de implementação.

Esses erros podem se tornar falhas misteriosas ou corrupção de dados, difíceis e demoradas de depurar. Então, este é um recurso importante, e vale a pena entender o contexto.

Preparando o cenário, com MyStringable

Primeiro, vamos considerar uma implementação simples do IStringable.

struct MyStringable : implements<MyStringable, IStringable>
{
    winrt::hstring ToString() const { return L"MyStringable"; }
};

Agora imagine que você precisa chamar uma função (de dentro de sua implementação) que espera um IStringable como um argumento.

void Print(IStringable const& stringable)
{
    printf("%ls\n", stringable.ToString().c_str());
}

O problema é que nosso tipo MyStringablenão é um IStringable.

  • Nosso tipo MyStringable é uma implementação da interface IStringable .
  • O tipo IStringable é um tipo projetado.

Important

É importante entender a distinção entre um tipo de implementação e um tipo projetado. Para conceitos e termos essenciais, não deixe de ler Consumir APIs com C++/WinRT e Criar APIs com C++/WinRT.

O espaço entre uma implementação e a projeção pode ser sutil de entender. E, de fato, para tentar fazer com que a implementação se pareça um pouco mais com a projeção, a própria implementação fornece conversões implícitas para cada um dos tipos projetados que ela implementa. Isso não significa que podemos simplesmente fazer isso.

struct MyStringable : implements<MyStringable, IStringable>
{
    winrt::hstring ToString() const;
 
    void Call()
    {
        Print(this);
    }
};

Em vez disso, precisamos obter uma referência para que os operadores de conversão possam ser usados como candidatos para resolver a chamada.

void Call()
{
    Print(*this);
}

Isso funciona. Uma conversão implícita fornece uma conversão (muito eficiente) do tipo de implementação para o tipo projetado, e isso é muito conveniente para muitos cenários. Sem essa instalação, muitos tipos de implementação se mostrariam muito complicados de criar. Desde que você use apenas o modelo de função winrt::make (ou winrt::make_self) para alocar a implementação, então tudo está bem.

IStringable stringable{ winrt::make<MyStringable>() };

Possíveis armadilhas com C++/WinRT 1.0

Ainda assim, conversões implícitas podem colocar você em apuros. Considere essa função auxiliar inútil.

IStringable MakeStringable()
{
    return MyStringable(); // Incorrect.
}

Ou mesmo apenas esta declaração aparentemente inofensiva.

IStringable stringable{ MyStringable() }; // Also incorrect.

Infelizmente, um código como esse foi compilado com C++/WinRT 1.0, devido a essa conversão implícita. O problema (muito sério) é que estamos potencialmente retornando um tipo projetado que aponta para um objeto contado por referência cuja memória de backup está na pilha efêmera.

Aqui está outra coisa que foi compilada com C++/WinRT 1.0.

MyStringable* stringable{ new MyStringable() }; // Very inadvisable.

Ponteiros raw são perigosos e exigem muito trabalho, além de serem uma fonte de bugs. Não os use se você não precisar. O C++/WinRT se esforça ao máximo para tornar tudo eficiente sem nunca obrigar você a usar ponteiros crus. Aqui está outra coisa que foi compilada com C++/WinRT 1.0.

auto stringable{ std::make_shared<MyStringable>(); } // Also very inadvisable.

Isso é um erro em vários níveis. Temos duas contagens de referência diferentes para o mesmo objeto. O Windows Runtime (e o COM clássico antes dele) se baseia em uma contagem de referência intrínseca que não é compatível com std::shared_ptr. std::shared_ptr tem, é claro, muitos aplicativos válidos; mas é totalmente desnecessário quando você está compartilhando objetos Windows Runtime (e COM clássicos). Por fim, isso também foi compilado com C++/WinRT 1.0.

auto stringable{ std::make_unique<MyStringable>() }; // Highly dubious.

Isso é novamente bastante questionável. A propriedade exclusiva está em oposição ao tempo de vida compartilhado da contagem de referência intrínseca do MyStringable.

A solução com C++/WinRT 2.0

Com o C++/WinRT 2.0, todas essas tentativas de alocar diretamente tipos de implementação levam a um erro do compilador. Esse é o melhor tipo de erro e infinitamente melhor do que um bug de runtime misterioso.

Sempre que precisar fazer uma implementação, você pode simplesmente usar winrt::make ou winrt::make_self, conforme mostrado acima. E agora, se você esquecer de fazer isso, verá um erro de compilação fazendo referência a isso, com uma referência a uma função abstrata chamada use_make_function_to_create_this_object. Não é exatamente um static_assert, mas chega perto. Ainda assim, essa é a maneira mais confiável de detectar todos os erros descritos.

Isso significa que precisamos colocar algumas restrições secundárias na implementação. Considerando que estamos contando com a ausência de uma substituição para detectar a alocação direta, o modelo de função winrt::make deve de alguma forma satisfazer a função virtual abstrata com uma substituição. Ele faz isso derivando da implementação com uma final classe que fornece a substituição. Há algumas coisas a serem observadas sobre esse processo.

Primeiro, a função virtual está presente apenas em compilações de depuração. O que significa que a detecção não afetará o tamanho da vtable nas suas compilações otimizadas.

Em segundo lugar, como a classe derivada que winrt::make usa é final, significa que qualquer devirtualização que o otimizador pode possivelmente deduzir ocorrerá mesmo se você optou anteriormente por não marcar sua classe de implementação como final. Então isso é uma melhoria. O inverso é que sua implementação não pode ser final. Novamente, isso não é consequência porque o tipo instanciado sempre será final.

Terceiro, nada impede que você marque funções virtuais em sua implementação como final. Claro, C++/WinRT é muito diferente do COM clássico e implementações como WRL, em que tudo sobre sua implementação tende a ser virtual. No C++/WinRT, o despacho virtual é limitado à ABI (interface binária de aplicativo) (que é sempre final), e seus métodos de implementação dependem de polimorfismo em tempo de compilação ou estático. Isso evita o polimorfismo de runtime desnecessário e também significa que há pouca razão para funções virtuais na implementação do C++/WinRT. O que é muito bom e resulta em um inlining muito mais previsível.

Em quarto lugar, já que winrt::make injeta uma classe derivada, sua implementação não pode ter um destruidor privado. Destrutores privados eram populares nas implementações COM clássicas porque, novamente, tudo era virtual, e era comum lidar diretamente com ponteiros brutos e, portanto, era fácil chamar acidentalmente delete em vez de Release. O C++/WinRT faz de tudo para dificultar que você lide diretamente com ponteiros brutos. E você teria que realmente se esforçar muito para obter um ponteiro bruto em C++/WinRT no qual você poderia potencialmente chamar delete. Semântica de valor significa que você está lidando com valores e referências; e raramente com ponteiros.

Portanto, c++/WinRT desafia nossas noções preconcebidas do que significa escrever código COM clássico. E isso é perfeitamente razoável porque WinRT não é COM clássico. COM clássico é a linguagem de montagem do Windows Runtime. Não deve ser o código que você escreve todos os dias. Em vez disso, O C++/WinRT faz com que você escreva um código mais parecido com c++moderno e muito menos como o COM clássico.

APIs importantes