Delen via


Diagnose van directe toewijzingen

Zoals uitgelegd in Author API's met C++/WinRT, moet u de winrt::make hulpmiddelenfamilie 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 leiden tot mysterieuze crashes of corrupties die moeilijk en tijdrovend zijn om op te lossen. 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 onze MyStringable type niet is een IStringable.

  • 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 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 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 zijn uiterste best om alles efficiënt te maken zonder dat u ooit gedwongen wordt om ruwe aanwijzers 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. De Windows Runtime (en daarvoor de klassieke COM) is gebaseerd op een intrinsiek referentieaantal dat 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-objecten (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 is in tegenstelling tot de gedeelde levensduur van de MyStringable's intrinsieke referentieaantal.

De oplossing met C++/WinRT 2.0

Met C++/WinRT 2.0 leiden al deze pogingen om implementatietypen rechtstreeks toe te wijzen 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 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 overschrijving om directe toewijzing te detecteren, moet de winrt::make sjabloon op een bepaalde manier voldoen aan de abstracte virtuele functie met een overschrijving. Het doet dit door af te leiden van de implementatie met een final klasse die de override 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 betekent het feit dat de afgeleide klasse die winrt::make gebruikt finalis, dat elke devirtualisatie die de optimizer kan afleiden waarschijnlijk plaatsvindt, ook als u eerder hebt besloten om uw implementatieklasse niet als finalte markeren. Dus dat is een verbetering. Het tegenovergestelde is dat uw implementatie-niet kan worden final. Nogmaals, dat heeft geen gevolgen omdat het type dat geïnstantieerd is altijd finalzal zijn.

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. Wat heel goed is en leidt tot veel voorspelbaardere inlining.

Ten vierde, omdat winrt::make een afgeleide klasse injecteert, kan uw implementatie geen privédestructor hebben. Privedestructors waren populair bij klassieke COM-implementaties omdat alles weer virtueel was, en het gebruikelijk was direct met ruwe pointers te werken, waardoor het gemakkelijk was om per ongeluk delete aan te roepen in plaats van Release. C++/WinRT doet uiterste best om het u moeilijk te maken om rechtstreeks met ruwe aanwijzers te werken. En je moet echt om een onbewerkte aanwijzer te krijgen in C++/WinRT die je mogelijk delete kunt 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