Diagnostika přímých přidělení

Jak je vysvětleno v Vytváření rozhraní API pomocí C++/WinRT, když vytváříte objekt implementačního typu, měli byste k tomu použít skupinu pomocných funkcí winrt::make. Toto téma podrobně popisuje funkci C++/WinRT 2.0, která vám pomůže diagnostikovat chybu přímého přidělování objektu typu implementace v zásobníku.

Takové chyby se můžou změnit na záhadné havárie nebo poškození, které jsou obtížné a časově náročné na ladění. Proto je to důležitá funkce, která stojí za pochopení pozadí.

Nastavení scény pomocí myStringable

Nejprve se podíváme na jednoduchou implementaci IStringable.

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

Teď si představte, že potřebujete v rámci své implementace zavolat funkci, která jako argument očekává IStringable.

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

Problém je, že náš typ MyStringableneníIStringable.

  • Náš MyStringable typ je implementace IStringable rozhraní.
  • Typ IStringable je projektovaný typ.

Důležité

Je důležité pochopit rozdíl mezi typem implementace a projektovaným typem. V případě základních konceptů a termínů si nezapomeňte přečíst rozhraní API pro používání pomocí C++/WinRT a rozhraní API pro vytváření v jazyce C++/WinRT.

Prostor mezi implementací a projekcí může být jemný pro pochopení. Aby se implementace ve skutečnosti alespoň trochu více podobala projekci, nabízí implicitní převody na každý z promítaných typů, které implementuje. To neznamená, že to prostě můžeme udělat.

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

Místo toho potřebujeme získat referenci, aby operátory převodu mohly být použity jako kandidáti při rozhodování o volání.

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

To funguje. Implicitní převod poskytuje (velmi efektivní) převod z typu implementace na projektovaný typ a je velmi pohodlný pro mnoho scénářů. Bez tohoto zařízení by se hodně typů implementace ukázalo jako velmi těžkopádné pro autora. Za předpokladu, že k přidělení implementace použijete pouze šablonu funkce winrt::make (nebo winrt::make_self), pak je vše v pořádku.

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

Potenciální nástrahy C++/WinRT 1.0

Implicitní převody vás ale mohou dostat do potíží. Zvažte tuto nepoužitou pomocnou funkci.

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

Nebo dokonce jen tento zdánlivě neškodný výrok.

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

Kód podobný tomu se bohužel zkompiloval pomocí C++/WinRT 1.0, protože tento implicitní převod. Problém (velmi závažný) spočívá v tom, že potenciálně vracíme promítnutý typ, který ukazuje na objekt s referenčním počítáním, jehož podkladová paměť se nachází na efemérním zásobníku.

Je tu ještě něco, co bylo zkompilováno pomocí C++/WinRT 1.0.

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

Nezpracované ukazatele jsou nebezpečným a pracně náročným zdrojem chyb. Nepoužívejte je, pokud je nepotřebujete. C++/WinRT dělá maximum pro to, aby vše bylo efektivní, aniž by vás kdy nutilo používat surové ukazatele. Je tu něco jiného, co se zkompilovalo pomocí C++/WinRT 1.0.

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

To je chyba na několika úrovních. Pro stejný objekt máme dva různé počty odkazů. prostředí Windows Runtime (a před ním i klasické COM) je založen na interním počítání referencí, které není kompatibilní s std::shared_ptr. std::shared_ptr má samozřejmě mnoho platných aplikací; ale při sdílení objektů prostředí Windows Runtime (a klasických com) je zcela zbytečné. Nakonec se tento příkaz zkompiloval také pomocí C++/WinRT 1.0.

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

To je opět poměrně pochybné. Jedinečné vlastnictví je v rozporu se sdílenou životností vnitřního referenčního počtu MyStringable.

Řešení s C++/WinRT 2.0

U C++/WinRT 2.0 všechny tyto pokusy o přímé přidělení typů implementace vedou k chybě kompilátoru. To je nejlepší druh chyby a nekonečně lepší než záhadná chyba modulu runtime.

Kdykoli potřebujete provést implementaci, můžete jednoduše použít winrt::make nebo winrt::make_self, jak je znázorněno výše. A teď, když na to zapomenete, zobrazí se vám chyba kompilátoru, která na to odkazuje pomocí abstraktní funkce s názvem use_make_function_to_create_this_object. Není to úplně static_assert, ale je to blízko. Přesto je to nejspolehlivější způsob zjištění všech popsaných chyb.

Znamená to, že musíme na implementaci umístit několik menších omezení. Vzhledem k tomu, že se při detekci přímé alokace spoléháme na nepřítomnost přepsání, musí šablona funkce winrt::make nějakým způsobem implementovat tuto abstraktní virtuální funkci pomocí přepsání. Dělá to odvozením z implementace pomocí třídy final, která toto přepsání implementuje. Je třeba si o tomto procesu všimnout několika věcí.

Za prvé je virtuální funkce přítomna pouze v ladicích sestaveních. To znamená, že detekce nebude mít vliv na velikost virtuální tabulky v optimalizovaných buildech.

Za druhé, protože odvozená třída, kterou winrt::make používá, je final, znamená to, že k jakékoli devirtualizaci, kterou optimalizátor dokáže odvodit, dojde, i když jste se dříve rozhodli neoznačit svou implementační třídu jako final. Takže to je zlepšení. Naopak vaše implementace nemůže být final. Opět na tom nezáleží, protože instancovaný typ bude vždy final.

Za třetí, nic vám nebrání v označení všech virtuálních funkcí ve vaší implementaci jako final. Samozřejmě, C++/WinRT se velmi liší od klasického COM a implementací, jako je WRL, kde bývá ve vaší implementaci prakticky vše virtuální. V jazyce C++/WinRT je virtuální odesílání omezeno na binární rozhraní aplikace (ABI) (což je vždy final) a vaše metody implementace spoléhají na kompilaci a statický polymorfismus. Tím se vyhnete zbytečnému runtime polymorfismu a také to znamená, že pro virtuální funkce je ve vaší implementaci C++/WinRT jen pramalý důvod. Což je velmi dobře a vede k mnohem lépe předvídatelnému inliningu.

Za čtvrté, protože winrt::make vloží odvozenou třídu, vaše implementace nemůže mít privátní destruktor. Privátní destruktory byly u klasických implementací COM oblíbené, protože opět bylo všechno virtuální a běžně se pracovalo přímo se syrovými ukazateli, takže bylo snadné omylem zavolat delete místo Release. C++/WinRT dělá vše pro to, aby vám co nejvíce znesnadnil přímou práci se surovými ukazateli. A museli byste se opravdu hodně snažit, abyste v C++/WinRT získali surový ukazatel, na kterém byste potenciálně mohli zavolat delete. Sémantika hodnot znamená, že pracujete s hodnotami a odkazy; zřídka s ukazateli.

C++/WinRT tedy zpochybňuje naše zažité představy o tom, co znamená psát klasický kód COM. A to je naprosto rozumné, protože WinRT není klasické COM. Klasický model COM je jazyk sestavení prostředí Windows Runtime. Neměl by to být kód, který píšete každý den. Místo toho vám C++/WinRT umožní psát kód, který je spíš moderní jazyk C++ a mnohem méně podobný klasickému modelu COM.

Důležitá rozhraní API