Diagnóstico de alocações diretas

Como explicado em APIs de autor com C++/WinRT, quando crias um objeto de tipo de implementação, deves usar a família de ajudantes winrt:: make para o fazer. Este tópico aborda em profundidade uma funcionalidade do C++/WinRT 2.0 que o ajuda a diagnosticar o erro de alocar diretamente na pilha um objeto de um tipo de implementação.

Tais erros podem transformar-se em falhas ou corrupções misteriosas, difíceis e demoradas de depurar. Por isso, esta é uma funcionalidade importante, e vale a pena compreender o contexto.

A preparar o contexto, com MyStringable

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

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

Agora imagina que precisas de chamar uma função (dentro da tua 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.

Important

É importante compreender a distinção entre um tipo de implementação e um tipo projetado. Para 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 subtil de compreender. E, de facto, 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 possamos simplesmente fazer isto.

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

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

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

Funciona. Uma conversão implícita proporciona uma conversão (muito eficiente) do tipo de implementação para o tipo projetado, e isso é muito conveniente para muitos cenários. Sem essa funcionalidade, muitos tipos de implementação seriam muito difíceis de escrever. Desde que uses apenas o modelo de função winrt::make (ou winrt::make_self) para alocar a implementação, então está tudo bem.

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

Potenciais armadilhas com C++/WinRT 1.0

Ainda assim, conversões implícitas podem meter-te em problemas. 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, código assim foi compilado com C++/WinRT 1.0, devido a essa conversão implícita. O problema (muito sério) é que estamos potencialmente a devolver um tipo projectado que aponta para um objeto com contagem de referências cuja memória subjacente está na pilha temporária.

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

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

Os apontadores brutos são uma fonte perigosa e laboriosa de bugs. Não os uses se não precisares. O C++/WinRT faz questão de tornar tudo eficiente sem nunca te obrigar a usar ponteiros brutos. Aqui está outra coisa que foi compilada com C++/WinRT 1.0.

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

Isto é um erro a 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) baseia-se numa contagem intrínseca de referências que não é compatível com std::shared_ptr. STD::shared_ptr tem, claro, muitas aplicações válidas; mas é totalmente desnecessário quando partilhas objetos do Windows Runtime (e COM clássico). Finalmente, este também foi compilado com C++/WinRT 1.0.

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

Isto é novamente bastante questionável. A propriedade única opõe-se à vida útil partilhada da contagem intrínseca de referências do MyStringable.

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

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

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

Significa que precisamos de impor algumas pequenas limitações à implementação. Dado que dependemos da ausência de um override para detetar a alocação direta, o template da função winrt::make deve de alguma forma satisfazer a função virtual abstrata com um override. Faz-no derivando da implementação com uma final classe que fornece o override. Há alguns aspetos a observar neste processo.

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

Em segundo lugar, uma vez que a classe derivada que o winrt::make usa é final, significa que qualquer desvirtualização que o otimizador possa deduzir acontecerá mesmo que anteriormente tenha optado por não marcar a sua classe de implementação como final. Portanto, isso é uma melhoria. O inverso é que a sua implementação não pode ser final. Mais uma vez, isso não tem importância porque o tipo instanciado será finalsempre .

Em terceiro lugar, nada o impede de marcar quaisquer funções virtuais na sua implementação como final. Claro que o C++/WinRT é muito diferente do COM clássico e implementações como o WRL, onde tudo na tua implementação tende a ser virtual. Em C++/WinRT, o despacho virtual está limitado à interface binária da aplicação (ABI) (que é sempre final), e os seus métodos de implementação dependem de polimorfismo em tempo de compilação ou estático. Isso evita polimorfismo desnecessário em tempo de execução e também significa que há muito pouca razão para funções virtuais na tua implementação de C++/WinRT. O que é muito bom e leva a um inlining muito mais previsível.

Quarto, como o winrt::make injeta uma classe derivada, a tua implementação não pode ter um destruidor privado. Os destruidores privados eram populares nas implementações clássicas de COM porque, mais uma vez, tudo era virtual, e era comum lidar diretamente com ponteiros em bruto, pelo que era fácil chamar acidentalmente delete em vez de Release. O C++/WinRT esforça-se por tornar difícil lidar diretamente com raw pointers. E terias mesmo de te esforçar para obter um ponteiro bruto em C++/WinRT no qual poderias potencialmente chamar delete. Semântica de valores significa que estás a lidar com valores e referências; E raramente com apontamentos.

Assim, C++/WinRT desafia as nossas noções pré-concebidas sobre o que significa escrever código COM clássico. E isso é perfeitamente razoável porque o WinRT não é o COM clássico. Classic COM é a linguagem assembly do Windows Runtime. Não devia ser o código que escreves todos os dias. Em vez disso, o C++/WinRT faz com que escrevas código mais parecido com o C++ moderno e muito menos com o COM clássico.

APIs importantes