Diagnostisera direktallokeringar

Som förklaras i Api:er för författare med C++/WinRT bör du använda winrt::make-familjen med hjälpverktyg när du skapar ett objekt av implementeringstyp. Det här avsnittet går djupare på en C++/WinRT 2.0-funktion som hjälper dig att diagnostisera misstaget att direkt allokera ett objekt av implementeringstyp på stacken.

Sådana misstag kan förvandlas till mystiska krascher eller skador som är svåra och tidskrävande att felsöka. Det här är därför en viktig funktion och det är värt att förstå bakgrunden.

Ställa in scenen med MyStringable

Först ska vi överväga en enkel implementering av IStringable.

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

Anta nu att du måste anropa en funktion (inifrån implementeringen) som förväntar sig en IStringable som argument.

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

Problemet är att vår MyStringable-typinte är en IStringable.

  • Vår MyStringable-typ är en implementering av IStringable-gränssnittet .
  • IStringable-typen är en projekterad typ.

Viktigt!

Det är viktigt att förstå skillnaden mellan en implementeringstyp och en beräknad typ. För viktiga begrepp och termer bör du läsa Använda API:er med C++/WinRT - och Author-API:er med C++/WinRT.

Skillnaden mellan en implementering och en projektion kan vara subtil och svår att uppfatta. Och för att försöka få implementeringen att kännas lite mer som projektionen ger implementeringen implicita konverteringar till var och en av de planerade typerna som implementeras. Det betyder inte att vi bara kan göra det här.

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

I stället måste vi hämta en referens så att konverteringsoperatorer kan användas som kandidater för att lösa anropet.

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

Det fungerar. En implicit konvertering ger en (mycket effektiv) konvertering från implementeringstypen till den planerade typen, och det är mycket praktiskt för många scenarier. Utan den möjligheten skulle många implementeringstyper visa sig vara mycket besvärliga att skapa. Förutsatt att du bara använder winrt:: make-funktionsmallen (eller winrt::make_self) för att allokera implementeringen är allt bra.

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

Potentiella fallgropar med C++/WinRT 1.0

Men implicita konverteringar kan få dig att hamna i trubbel. Tänk på den här hjälpfunktionen som inte hjälper dig.

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

Eller till och med bara detta till synes ofarliga uttalande.

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

Tyvärr kompilerades kod som sådan med C++/WinRT 1.0 på grund av den implicita konverteringen. Det (mycket allvarliga) problemet är att vi potentiellt returnerar en projicerad typ som pekar på ett referensräknat objekt vars underliggande minne finns på den temporära stacken.

Här är något annat som kompilerats med C++/WinRT 1.0.

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

Råa pekare är en farlig och arbetsintensiv källa till buggar. Använd dem inte om du inte behöver det. C++/WinRT gör allt effektivt utan att någonsin tvinga dig att använda råpekare. Här är något annat som kompilerats med C++/WinRT 1.0.

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

Detta är ett misstag på flera nivåer. Vi har två olika referensantal för samma objekt. Windows Runtime (och klassisk COM före den) baseras på ett inbyggt referensantal som inte är kompatibelt med std::shared_ptr. std::shared_ptr har naturligtvis många giltiga program; men det är helt onödigt när du delar Windows Runtime (och klassiska COM)-objekt. Slutligen kompilerades detta också med C++/WinRT 1.0.

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

Detta är återigen ganska tvivelaktigt. Det unika ägandet står i motsats till den delade livslängden hos MyStringable:s inneboende referensräkning.

Lösningen med C++/WinRT 2.0

Med C++/WinRT 2.0 leder alla dessa försök att direkt allokera implementeringstyper till ett kompilatorfel. Det är det bästa slags fel, och oändligt mycket bättre än ett mystiskt körtidsfel.

När du behöver göra en implementering kan du helt enkelt använda winrt::make eller winrt::make_self, som du ser ovan. Och nu, om du glömmer att göra det, kommer du att hälsas med ett kompilatorfel som anspelar på detta med en referens till en abstrakt funktion med namnet use_make_function_to_create_this_object. Det är inte precis en static_assert, men det är nära. Ändå är detta det mest tillförlitliga sättet att upptäcka alla misstag som beskrivs.

Det innebär att vi måste införa några mindre begränsningar för genomförandet. Med tanke på att vi förlitar oss på avsaknaden av en override för att upptäcka direktallokering måste funktionsmallen winrt::make på något sätt tillhandahålla en override för den abstrakta virtuella funktionen. Detta görs genom att ärva från implementeringen med en klass final som innehåller åsidosättningen. Det finns några saker att observera om den här processen.

För det första finns den virtuella funktionen bara i felsökningsversioner. Vilket innebär att detektering inte kommer att påverka storleken på vtable i dina optimerade byggen.

För det andra, eftersom den härledda klassen som winrt::make använder är finalinnebär det att alla devirtualiseringar som optimeraren eventuellt kan härleda sker även om du tidigare valde att inte markera implementeringsklassen som final. Så det är en förbättring. Det omvända är att implementeringen inte kan vara final. Återigen har det ingen betydelse eftersom den instansierade typen alltid kommer att vara final.

För det tredje hindrar ingenting dig från att markera några virtuella funktioner i implementeringen som final. Naturligtvis skiljer sig C++/WinRT mycket från klassisk COM och implementeringar som WRL, där allt om din implementering tenderar att vara virtuellt. I C++/WinRT är den virtuella sändningen begränsad till det binära programgränssnittet (ABI) (som alltid finalär ), och implementeringsmetoderna förlitar sig på kompileringstid eller statisk polymorfism. Det undviker onödig körningspolymorfism och innebär också att det finns mycket liten anledning till virtuella funktioner i C++/WinRT-implementeringen. Vilket är väldigt bra och leder till betydligt mer förutsägbar inlining.

För det fjärde, eftersom winrt::make matar in en härledd klass, kan implementeringen inte ha en privat destruktor. Privata destruktorer var populära med klassiska COM-implementeringar eftersom allt återigen var virtuellt, och det var vanligt att hantera råa pekare direkt och därför var lätt att av misstag anropa delete istället för Release. C++/WinRT anstränger sig för att göra det svårt för dig att hantera råpekare direkt. Och du skulle behöva anstränga dig ordentligt för att få en rå pekare i C++/WinRT som du potentiellt skulle kunna anropa delete på. Värdesemantik innebär att du hanterar värden och referenser. och sällan med pekare.

Därför utmanar C++/WinRT våra förutfattade meningar om vad det innebär att skriva klassisk COM-kod. Och det är helt rimligt eftersom WinRT inte är klassisk COM. Klassisk COM är sammansättningsspråket för Windows Runtime. Det borde inte vara koden du skriver varje dag. I stället får C++/WinRT dig att skriva kod som mer liknar modern C++, och mycket mindre som klassisk COM.

Viktiga API:er