Diagnostizieren direkter Zuordnungen

Wie in Autoren-APIs mit C++/WinRT erläutert, sollten Sie beim Erstellen eines Implementierungstypobjekts die Winrt::make-Familie von Helfern verwenden, um dies zu tun. Dieses Thema befasst sich ausführlich mit einem C++/WinRT 2.0-Feature, das Ihnen hilft, den Fehler zu diagnostizieren, ein Objekt des Implementierungstyps direkt im Stapel zuzuordnen.

Solche Fehler können zu mysteriösen Abstürze oder Beschädigungen werden, die schwierig und zeitaufwendig zu debuggen sind. Dies ist also ein wichtiges Feature, und es lohnt sich, den Hintergrund zu verstehen.

Festlegen der Szene mit MyStringable

Betrachten wir zunächst eine einfache Implementierung von IStringable.

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

Stellen Sie sich nun vor, dass Sie innerhalb Ihrer Implementierung eine Funktion aufrufen müssen, die ein IStringable als Argument erwartet.

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

Das Problem besteht darin, dass unser MyStringable-TypkeinIStringable-Typ ist.

  • Unser MyStringable-Typ ist eine Implementierung der IStringable-Schnittstelle .
  • Der IStringable-Typ ist ein projizierter Typ.

Important

Es ist wichtig, den Unterschied zwischen einem Implementierungstyp und einem projizierten Typ zu verstehen. Für wichtige Konzepte und Begriffe lesen Sie unbedingt APIs mit C++/WinRT nutzen und APIs mit C++/WinRT erstellen.

Der Raum zwischen einer Implementierung und der Projektion kann subtil zu erfassen sein. Und um zu versuchen, die Implementierung etwas ähnlich wie die Projektion zu gestalten, stellt die Implementierung implizite Konvertierungen zu jedem der projizierten Typen bereit, die sie implementiert. Das bedeutet nicht, dass wir dies einfach tun können.

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

Stattdessen müssen wir einen Verweis abrufen, damit Konvertierungsoperatoren als Kandidaten zum Auflösen des Anrufs verwendet werden können.

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

Das funktioniert. Eine implizite Konvertierung stellt eine (sehr effiziente) Konvertierung vom Implementierungstyp in den projizierten Typ bereit, und das ist für viele Szenarien sehr praktisch. Ohne diese Möglichkeit wären viele Implementierungstypen nur sehr umständlich zu erstellen. Vorausgesetzt, Sie verwenden nur die Winrt::make-Funktionsvorlage (oder winrt::make_self), um die Implementierung zuzuweisen, dann ist alles gut.

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

Potenzielle Fallstricke mit C++/WinRT 1.0

Dennoch können implizite Konvertierungen Sie in Schwierigkeiten bringen. Betrachten Sie diese nicht hilfreiche Hilfsfunktion.

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

Oder auch nur diese scheinbar harmlose Aussage.

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

Leider ließ sich solcher Code mit C++/WinRT 1.0 doch kompilieren, und zwar aufgrund dieser impliziten Konvertierung. Das (sehr schwerwiegende) Problem besteht darin, dass wir potenziell einen projizierten Typ zurückgeben, der auf ein Objekt mit Referenzzählung verweist, dessen zugrunde liegender Speicher auf dem temporären Stack liegt.

Hier ist etwas anderes, das mit C++/WinRT 1.0 kompiliert wurde.

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

Unformatierte Zeiger sind gefährlich und arbeitsintensive Fehlerquelle. Verwenden Sie sie nicht, wenn Sie sie nicht benötigen. C++/WinRT setzt alles daran, alles effizient zu gestalten, ohne Sie jemals dazu zu zwingen, Rohzeiger zu verwenden. Hier ist etwas anderes, das mit C++/WinRT 1.0 kompiliert wurde.

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

Dies ist ein Fehler auf mehreren Ebenen. Für dasselbe Objekt gibt es zwei unterschiedliche Verweisanzahlen. Windows-Runtime (und das klassische COM vor ihr) basiert auf einer intrinsischen Referenzzählung, die nicht mit std::shared_ptr kompatibel ist. std::shared_ptr hat natürlich viele gültige Anwendungen; Es ist jedoch völlig unnötig, wenn Sie Windows-Runtime (und klassische COM)-Objekte freigeben. Schließlich wurde dies auch mit C++/WinRT 1.0 kompiliert.

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

Das ist wieder fragwürdig. Der eindeutige Besitz steht gegen die gemeinsame Lebensdauer der systeminternen Referenzanzahl von MyStringable.

Die Lösung mit C++/WinRT 2.0

Bei C++/WinRT 2.0 führen alle diese Versuche, Implementierungstypen direkt zuzuordnen, zu einem Compilerfehler. Das ist die beste Art von Fehler und unendlich besser als ein mysteriöser Laufzeitfehler.

Wann immer Sie eine Implementierung vornehmen müssen, können Sie einfach "winrt::make " oder "winrt::make_self" verwenden, wie oben gezeigt. Und wenn Sie das nun vergessen, erhalten Sie einen Compilerfehler, der darauf hindeutet, und zwar mit einem Verweis auf eine abstrakte Funktion namens use_make_function_to_create_this_object. Es ist nicht ganz ein static_assert, aber es kommt dem nahe. Dennoch ist dies die zuverlässigste Möglichkeit, alle beschriebenen Fehler zu erkennen.

Es bedeutet, dass wir einige kleinere Einschränkungen für die Umsetzung setzen müssen. Da wir uns auf das Fehlen einer Überschreibung verlassen, um die direkte Allokation zu erkennen, muss die Funktionsvorlage winrt::make die abstrakte virtuelle Funktion irgendwie durch eine Überschreibung erfüllen. Dies geschieht, indem von einer Implementierung mit einer final-Klasse abgeleitet wird, die die Überschreibung bereitstellt. Bei diesem Prozess sind einige Dinge zu beachten.

Zunächst ist die virtuelle Funktion nur in Debugbuilds vorhanden. Dies bedeutet, dass sich die Erkennung nicht auf die Größe der vtable in Ihren optimierten Builds auswirkt.

Zweitens bedeutet die Tatsache, dass die von winrt::make verwendete abgeleitete Klasse final ist, dass jede Devirtualisierung, die der Optimierer möglicherweise ableiten kann, selbst dann erfolgt, wenn Sie sich zuvor entschieden haben, Ihre Implementierungsklasse nicht als final zu kennzeichnen. Das ist also eine Verbesserung. Das Gegenteil ist, dass Ihre Implementierung nicht sein finalkann. Auch hier ist das keine Folge, weil der instanziierte Typ immer sein finalwird.

Drittens verhindert nichts, dass Sie virtuelle Funktionen in Ihrer Implementierung als finalmarkieren. Natürlich unterscheidet sich C++/WinRT stark von klassischem COM und Implementierungen wie WRL, bei denen in der Regel alles an Ihrer Implementierung virtuell ist. In C++/WinRT ist der virtuelle Verteiler auf die binäre Anwendungsschnittstelle (ABI) (was immer final) beschränkt ist, und Ihre Implementierungsmethoden basieren auf Kompilierungszeit oder statischem Polymorphismus. Dies vermeidet unnötigen Laufzeitpolymorphismus und bedeutet außerdem, dass es in Ihrer C++/WinRT-Implementierung kaum einen Grund für virtuelle Funktionen gibt. Das ist eine sehr gute Sache und führt zu viel vorhersehbarerem Inlining.

Viertens, da winrt::make eine abgeleitete Klasse injiziert, kann Ihre Implementierung keinen privaten Destruktor haben. Private Destruktoren waren bei klassischen COM-Implementierungen beliebt, weil wiederum alles virtuell war und man üblicherweise direkt mit Rohzeigern arbeitete, sodass man leicht versehentlich delete statt Release aufrief. C++/WinRT unternimmt besonderen Aufwand, um es Ihnen schwer zu machen, direkt mit rohen Zeigern umzugehen. Und man müsste sich wirklich besonders anstrengen, um in C++/WinRT einen rohen Zeiger zu erhalten, auf dem man potenziell delete aufrufen könnte. Die Wertsemantik bedeutet, dass Sie mit Werten und Verweisen arbeiten; und selten mit Zeigern.

Daher fordert C++/WinRT unsere vorkonzessionierten Vorstellungen davon ab, was es bedeutet, klassischen COM-Code zu schreiben. Und das ist absolut vernünftig, weil WinRT nicht klassisch COM ist. Klassische COM ist die Assemblysprache des Windows-Runtime. Es sollte nicht der Code sein, den Sie täglich schreiben. Stattdessen können Sie mit C++/WinRT Code schreiben, der eher wie modernes C++ und viel weniger wie klassische COM aussieht.

Wichtige APIs