Compartilhar via


Diagnosticando alocações diretas

Conforme explicado em Criar APIs comC++/WinRT, ao criar um objeto de tipo de implementação, você deve usar o conjunto de auxiliares winrt::make para fazer isso. Este tópico se aprofunda em um recurso do C++/WinRT 2.0 que ajuda a diagnosticar o erro de alocar diretamente uma instância do tipo de implementação na pilha.

Esses erros podem se transformar em falhas misteriosas ou corrompimentos que são difíceis e demorados para depurar. Portanto, esse é um recurso importante e vale a pena compreender o contexto.

Definindo a cena, com MyStringable

Primeiro, vamos considerar uma implementação simples de 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 IStringable como argumento.

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

O problema é que nosso tipo MyStringable não é umIStringable.

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

Importante

É importante entender a distinção entre um tipo de implementação e um tipo projetado . Para entender conceitos e termos essenciais, certifique-se 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 fazer a implementação se assemelhar mais à projeção, ela fornece conversões implícitas para cada um dos tipos projetados que 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 esse recurso, muitos tipos de implementação provariam ser 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 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 não-gerenciados são uma fonte perigosa e trabalhosa de bugs. Não os use se você não precisar. O C++/WinRT se esforça para tornar tudo eficiente sem nunca forçar você a usar ponteiros nativos. 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, (assim como o COM clássico antes dele), é baseado em um sistema de 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 do Windows Runtime (e COM clássico). 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 se opõe 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 do compilador aludindo 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 está 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 um override para detectar a alocação direta, o modelo de função winrt::make deve, de alguma forma, satisfazer a função virtual abstrata com um override. Ele faz isso derivando da implementação com uma classe final que fornece a substituição. Há algumas coisas a serem observadas sobre esse processo.

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

Em segundo lugar, uma vez que a classe derivada que winrt::make usa é final, isso significa que qualquer devirtualização que o otimizador pode possivelmente deduzir ocorrerá mesmo que você tenha optado anteriormente por não marcar sua classe de implementação como final. Então isso é uma melhoria. O inverso é que seu de implementação não pode ser final. Novamente, isso não tem importância porque o tipo instanciado será sempre 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, a chamada virtual é limitada à interface binária do aplicativo (ABI) (que é sempre final) e seus métodos de implementação contam com polimorfismo estático ou de tempo de compilação. 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. Algo muito bom, que resulta em um inline muito mais previsível.

Em quarto lugar, como winrt::make injeta uma classe derivada, sua implementação não pode ter um destruidor privado. Os destrutores privados eram populares nas implementações clássicas de COM porque, novamente, tudo era virtual, e era comum lidar diretamente com ponteiros brutos, portanto, era fácil chamar acidentalmente delete em vez de Release. O C++/WinRT faz um grande esforço para tornar difícil lidar diretamente com ponteiros brutos. E você teria que realmente sair do seu caminho para obter um ponteiro bruto em C++/WinRT que 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 é o COM clássico. COM clássico é a linguagem assembly 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