Partilhar via


Diagnóstico de alocações diretas

Conforme explicado em APIs de autor com C++/WinRT, quando você cria um objeto do tipo de implementação, você deve usar o winrt::make família de auxiliares para fazer isso. Este tópico aprofunda-se numa funcionalidade da versão 2.0 do C++/WinRT que ajuda a diagnosticar o erro de alocar diretamente um objeto do tipo de implementação na pilha.

Tais erros podem se transformar em falhas misteriosas ou corrupções que são difíceis e demoradas de depurar. Portanto, esta é uma característica importante, e vale a pena entender o contexto.

Preparando o cenário, 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 da sua implementação) que espera um IStringable como argumento.

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

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

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

Importante

É importante entender a distinção entre um tipo de implementação e um tipo projetado. Para conceitos e termos essenciais, leia 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 pareça um pouco mais com a projeção, a implementação 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 essa facilidade, muitos tipos de implementação seriam muito complicados para o autor. 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>() };

Armadilhas potenciais com C++/WinRT 1.0

Ainda assim, conversões implícitas podem colocá-lo em apuros. Considere esta função auxiliar inútil.

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

Ou mesmo apenas esta afirmação aparentemente inofensiva.

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

Infelizmente, um código como esse compilado com C++/WinRT 1.0, por causa dessa 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 suporte está na pilha efêmera.

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

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

Os ponteiros brutos são uma fonte perigosa e trabalhosa de bugs. Não os use se não precisar. C++/WinRT esforça-se para tornar tudo eficiente sem nunca forçar você a usar ponteiros brutos. Aqui está outra coisa compilada com C++/WinRT 1.0.

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

Trata-se de um erro a vários níveis. Temos duas contagens de referência diferentes para o mesmo objeto. O Tempo de Execução do Windows (e o COM clássico antes dele) é baseado numa contagem de referências intrínseca que não é compatível com std::shared_ptr. std::shared_ptr tem, naturalmente, muitas aplicações válidas; mas é totalmente desnecessário quando você compartilha objetos do Tempo de Execução do Windows (e COM clássico). Finalmente, isso foi também compilado com C++/WinRT 1.0.

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

Isto é, mais uma vez, bastante questionável. A propriedade exclusiva contrapõe-se à vida útil partilhada 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 os tipos de implementação levam a um erro do compilador. Esse é o melhor tipo de erro, e infinitamente melhor do que um misterioso bug de tempo de execução.

Sempre que você precisar fazer uma implementação, você pode simplesmente usar winrt::make ou winrt::make_self, como mostrado acima. E agora, se você esquecer de fazer isso, então você será recebido com um erro de compilador aludindo a isso com uma referência a uma função abstrata chamada use_make_function_to_create_this_object. Não é bem uma static_assert, mas está perto. Ainda assim, esta é a forma mais fiável de detetar todos os erros descritos.

Significa que temos de impor algumas pequenas restrições à implementação. Dado que estamos confiando na ausência de uma substituição para detetar 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 classe final que fornece a substituição. Há algumas coisas a observar sobre este processo.

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

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

Em terceiro lugar, nada impede que você marque quaisquer funções virtuais em sua implementação como final. É claro que o C++/WinRT é muito diferente do COM clássico e de implementações como WRL, onde tudo sobre sua implementação tende a ser virtual. Em C++/WinRT, o despacho virtual é limitado à interface binária da aplicação (ABI) (que é sempre final), e os seus métodos de implementação dependem de polimorfismo estático ou em tempo de compilação. Isso evita o polimorfismo de tempo de execução desnecessário e também significa que há muito pouca razão para funções virtuais na sua implementação de C++/WinRT. O que é muito bom e, por consequência, resulta em um encadeamento 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 em implementações clássicas de COM porque, novamente, tudo era virtual, e era comum trabalhar diretamente com ponteiros diretos e, portanto, era fácil chamar acidentalmente delete em vez de Release. C++/WinRT faz de tudo para tornar difícil para você 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 chamádelete lo. Semântica de valor significa que você está lidando com valores e referências; e raramente com ponteiros.

Assim, C++/WinRT desafia nossas noções preconcebidas do que significa escrever código COM clássico. E isso é perfeitamente razoável porque o WinRT não é o COM clássico. COM clássico é a linguagem de montagem do Tempo de Execução do Windows. 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 o C++ moderno e muito menos com o COM clássico.

APIs importantes