Diagnose van directe toewijzingen

Zoals uitgelegd in Author-API's met C++/WinRT, moet u bij het maken van een object van het implementatietype de winrt::make family of helpers gebruiken om dit te doen. In dit onderwerp wordt dieper ingegaan op een C++/WinRT 2.0-functie waarmee u de fout kunt vaststellen van het rechtstreeks toewijzen van een object van het implementatietype op de stack.

Dergelijke fouten kunnen uitmonden in mysterieuze crashes of gegevenscorruptie die moeilijk en tijdrovend te debuggen zijn. Dit is dus een belangrijke functie en het is de moeite waard om de achtergrond te begrijpen.

De scène instellen met MyStringable

Laten we eerst een eenvoudige implementatie van IStringable overwegen.

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

Stel nu dat u een functie (vanuit uw implementatie) moet aanroepen die een IStringable als argument verwacht.

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

Het probleem is dat ons type MyStringableniet een IStringable is.

  • Ons type MyStringable is een implementatie van de IStringable-interface .
  • Het type IStringable is een geprojecteerd type.

Belangrijk

Het is belangrijk om inzicht te hebben in het onderscheid tussen een implementatietype en een projected type. Lees voor essentiële concepten en termen zeker API's gebruiken met C++/WinRT en API's ontwerpen met C++/WinRT.

De ruimte tussen een implementatie en de projectie kan subtiel zijn om te begrijpen. En om te proberen de implementatie iets meer te laten lijken op de projectie, biedt de implementatie impliciete conversies naar elk van de verwachte typen die worden geïmplementeerd. Dat betekent niet dat we dit gewoon kunnen doen.

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

In plaats daarvan moeten we een verwijzing krijgen, zodat conversieoperators kunnen worden gebruikt als kandidaten voor het oplossen van de oproep.

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

Dat werkt. Een impliciete conversie biedt een (zeer efficiënte) conversie van het implementatietype naar het verwachte type, en dat is erg handig voor veel scenario's. Zonder die faciliteit zouden veel implementatietypen erg lastig blijken te zijn om te schrijven. Mits u alleen de winrt::make-functiesjabloon (of winrt::make_self) gebruikt om de implementatie toe te wijzen, is alles goed.

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

Mogelijke valkuilen met C++/WinRT 1.0

Impliciete conversies kunnen u toch in problemen brengen. Houd rekening met deze niet-helpende helperfunctie.

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

Of zelfs zo'n ongevaarlijke verklaring.

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

Helaas heeft code zoals die is gecompileerd met C++/WinRT 1.0, vanwege die impliciete conversie. Het (zeer ernstige) probleem is dat we mogelijk een geprojecteerd type retourneren dat verwijst naar een verwijzingsgeteld object waarvan het back-upgeheugen zich op de tijdelijke stack bevindt.

Hier volgt nog iets dat is gecompileerd met C++/WinRT 1.0.

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

Onbewerkte aanwijzers zijn gevaarlijke en arbeidsintensieve bron van bugs. Gebruik ze niet als u dat niet nodig hebt. C++/WinRT doet er alles aan om alles efficiënt te maken zonder u ooit te dwingen raw pointers te gebruiken. Hier volgt nog iets dat is gecompileerd met C++/WinRT 1.0.

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

Dit is een fout op verschillende niveaus. We hebben twee verschillende referentieaantallen voor hetzelfde object. Windows Runtime (en de klassieke COM die eraan voorafging) is gebaseerd op een intrinsieke referentietelling die niet compatibel is met std::shared_ptr. std::shared_ptr heeft natuurlijk veel geldige toepassingen; maar het is helemaal niet nodig wanneer u Windows Runtime (en klassieke COM)-objecten deelt. Ten slotte is dit ook gecompileerd met C++/WinRT 1.0.

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

Dit is weer nogal twijfelbaar. Het unieke eigendom staat tegenover de gedeelde levensduur van de intrinsieke referentietelling van MyStringable.

De oplossing met C++/WinRT 2.0

Met C++/WinRT 2.0 leiden al deze pogingen om implementatietypen rechtstreeks te alloceren tot een compilerfout. Dat is het beste soort fout, en oneindig beter dan een mysterieuze runtimefout.

Wanneer u een implementatie moet maken, kunt u gewoon winrt::make of winrt::make_self gebruiken, zoals hierboven wordt weergegeven. En als u dit vergeet te doen, wordt u begroet met een compilerfout die hierop verwijst met een verwijzing naar een abstracte functie met de naam use_make_function_to_create_this_object. Het is niet precies een static_assert; maar het komt dicht in de buurt. Toch is dit de meest betrouwbare manier om alle beschreven fouten te detecteren.

Het betekent wel dat we een paar kleine beperkingen moeten stellen voor de tenuitvoerlegging. Aangezien we afhankelijk zijn van het ontbreken van een override om directe toewijzing te detecteren, moet de functiesjabloon winrt::make op de een of andere manier een override voor de abstracte virtuele functie bieden. Dit gebeurt door van de implementatie over te erven met een klasse final die de overschrijving biedt. Er zijn enkele dingen die u moet observeren over dit proces.

Ten eerste is de virtuele functie alleen aanwezig in builds voor foutopsporing. Dit betekent dat detectie niet van invloed is op de grootte van de vtable in uw geoptimaliseerde builds.

Ten tweede, aangezien de afgeleide klasse die winrt::make gebruikt final is, houdt dit in dat elke devirtualisatie die de optimizer mogelijkerwijs kan afleiden, zal plaatsvinden, zelfs als u er eerder voor hebt gekozen uw implementatieklasse niet als final te markeren. Dus dat is een verbetering. Het omgekeerde is dat uw implementatie niet kan zijn final. Nogmaals, dat is geen gevolg omdat het geïnstantieerde type altijd zal zijn final.

Ten derde, niets voorkomt dat u virtuele functies in uw implementatie markeert als final. Natuurlijk is C++/WinRT heel anders dan klassieke COM en implementaties zoals WRL, waarbij alles over uw implementatie meestal virtueel is. In C++/WinRT is de virtuele verzending beperkt tot de binaire interface (ABI) van de toepassing (altijd) finalen uw implementatiemethoden zijn afhankelijk van compileertijd of statisch polymorfisme. Dat voorkomt onnodige runtime polymorfisme en betekent ook dat er weinig reden is voor virtuele functies in uw C++/WinRT-implementatie. Dat is heel goed en leidt tot veel voorspelbarere inlining.

Ten vierde, omdat winrt::make een afgeleide klasse injecteert, kan uw implementatie geen privédestructor hebben. Private destructors waren populair bij klassieke COM-implementaties omdat, nogmaals, alles virtueel was en het gebruikelijk was om rechtstreeks met ruwe pointers te werken, waardoor je gemakkelijk per ongeluk delete aanriep in plaats van Release. C++/WinRT doet er alles aan om het je moeilijk te maken direct met ruwe pointers te werken. En je zou echt erg je best moeten doen om in C++/WinRT een raw pointer te krijgen waarop je mogelijk delete zou kunnen aanroepen. Waardesemantiek betekent dat u te maken hebt met waarden en verwijzingen; en zelden met aanwijzers.

C++/WinRT daagt dus onze vooroordelen uit over wat het betekent om klassieke COM-code te schrijven. En dat is perfect redelijk omdat WinRT geen klassieke COM is. Klassieke COM is de assemblytaal van de Windows Runtime. Dit mag niet de code zijn die u elke dag schrijft. In plaats daarvan kunt u met C++/WinRT code schrijven die meer lijkt op moderne C++, en veel minder op klassieke COM.

Belangrijke API's