Windows mit C++
Intelligente COM-Zeiger die Zweite
Dank der Wiedergeburt von COM, auch als Windows-Runtime bekannt, ist der Bedarf für einen effizienten und zuverlässigen, intelligenten Zeiger für COM-Schnittstellen größer denn je. Aber was muss ein guter intelligenter COM-Zeiger mitbringen? Tatsächlich ist die ATL CComPtr-Klassenvorlage schon seit gefühlten Jahrzehnten der intelligente COM-Zeiger. Mit dem Windows SDK für Windows 8 wurde die ComPtr-Klassenvorlage als Teil der Windows-Runtime C++-Vorlagenbibliothek (WRL) eingeführt, die von manchem als moderner Ersatz für den ATL CComPtr begrüßt wurde. Anfangs dachte ich auch, dass dies ein guter Schritt in die richtige Richtung sei, doch nach einer Menge Erfahrungen im Umgang mit dem WRL ComPtr bin ich zu dem Schluss gekommen, dass man ihn besser vermeiden sollte. Warum ist das so? Lesen Sie weiter.
Was sollten wir also unternehmen? Sollen wir zu ATL zurückkehren? Auf keinen Fall! Aber vielleicht ist es an der Zeit, einige moderne C++-Methoden, die von Visual C++ 2015 bereitgestellt werden, für das Design eines neuen intelligenten Zeigers für COM-Schnittstellen zu nutzen. In der Sonderausgabe zu "Connect(); Visual Studio 2015 & Microsoft Azure" habe ich gezeigt, wie man Visual C++ 2015 optimal nutzen kann, um ganz einfach "IUnknown" und "IInspectable" mithilfe der Implements-Klassenvorlage zu implementieren. Jetzt werde ich Ihnen zeigen, wie Sie mithilfe weiterer Funktionen von Visual C++ 2015 eine neue ComPtr-Klassenvorlage implementieren können.
Intelligente Zeiger sind bekanntermaßen schwierig zu schreiben, aber dank C++11 ist das bei Weitem nicht mehr so schwer wie früher. Der Grund hierfür hat teilweise mit all den cleveren Tricks zu tun, die sich Bibliotheksentwickler haben einfallen lassen, um die mangelnde Ausdrucksfähigkeit von C++ und den Standardbibliotheken zu umgehen, damit sich eigene Objekte wie integrierte Zeiger verhalten, gleichzeitig aber immer noch effizient und korrekt sind. Insbesondere rvalue-Verweise tragen in hohem Maße dazu bei, uns Bibliotheksentwicklern das Leben so sehr zu erleichtern. Eine weitere Komponente ist einfach unser Blick auf Vergangenes – das Wissen darum, wie es bestehenden Designs ergangen ist. Und natürlich haben wir es mit dem Dilemma aller Entwickler zu tun: Zurückhaltung zeigen und nicht versuchen, alle vorstellbaren Funktionen in eine bestimmte Abstraktion zu packen.
Ganz einfach betrachtet muss ein intelligenter COM-Zeiger die Ressourcenverwaltung für den zugrunde liegenden COM-Schnittstellenzeiger bereitstellen. Dies impliziert, dass der intelligente Zeiger eine Klassenvorlage sein und einen Schnittstellenzeiger des gewünschten Typs speichern muss. Technisch muss er gar nicht wirklich einen Schnittstellenzeiger eines bestimmten Typs speichern, sondern könnte stattdessen einfach einen IUnknown-Schnittstellenzeiger speichern. Dann müsste der intelligente Zeiger aber immer ein static_cast verwenden, wenn der intelligente Zeiger dereferenziert wird. Dies kann nützlich und gleichzeitig konzeptionell gefährlich sein, aber ich werde die noch in einer zukünftigen Kolumne behandeln. Vorläufig werde ich erst einmal mit einer grundlegenden Klassenvorlage zum Speichern eines stark typisierten Zeigers beginnen:
template <typename Interface>
class ComPtr
{
public:
ComPtr() noexcept = default;
private:
Interface * m_ptr = nullptr;
};
Langjährige C++-Entwickler werden sich vielleicht anfangs fragen, was das soll, aber aller Wahrscheinlichkeit nach werden die meisten aktiven C++-Entwickler nicht allzu erstaunt sein. Die m_ptr-Membervariable basiert auf einer großartigen neuen Funktion, die es erlaubt, nicht statische Datenmember bei ihrer Deklaration zu initialisieren. Dies verringert das Risiko, die Initialisierung von Membervariablen versehentlich zu vergessen, wenn im Laufe der Zeit Konstruktoren hinzugefügt und geändert werden, dramatisch. Jede Initialisierung, die von einem bestimmten Konstruktor ausdrücklich bereitgestellt wird, hat Vorrang vor dieser direkten Initialisierung, doch meistens bedeutet dies, dass sich Konstruktoren Gedanken um das Festlegen solcher Membervariablen machen müssen, die sonst andernfalls mit unvorhersehbaren Werten begonnen hätten.
Vorausgesetzt, dass die Initialisierung des Schnittstellenzeigers jetzt sichergestellt ist, kann ich mich ebenfalls einer weiteren Funktion bedienen, um eine Standarddefinition von speziellen Memberfunktionen anzufordern. Im vorherigen Beispiel fordere ich die Standarddefinition des Standardkonstruktors an – ein standardmäßiger Standardkonstruktor, wenn Sie so möchten. Der Überbringer der Nachricht kann nichts dafür. Dennoch gehören die Möglichkeit, spezielle Memberfunktionen mit einem Standardwert zu versehen oder zu löschen, in Verbindung mit der Fähigkeit zur Initialisierung von Membervariablen zum Zeitpunkt ihrer Deklaration zu meinen Lieblingsfunktionen, die Visual C++ 2015 zu bieten hat. Es sind eben die Kleinigkeiten, die zählen.
Der wichtigste Dienst, den ein intelligenter COM-Zeiger anbieten muss, ist es, den Entwickler vor den Gefahren des intrusiven COM-Verweiszählungsmodells zu schützen. Eigentlich mag ich den COM-Ansatz bei der Verweiszählung, doch ich möchte, dass das eine Bibliothek für mich übernimmt. Dies tritt an einer Reihe subtiler Stellen in der gesamten ComPtr-Klassenvorlage zutage, doch am offensichtlichsten ist es vielleicht, wenn ein Aufrufer den intelligenten Zeiger dereferenziert. Ich möchte nicht, dass ein Aufrufer etwas wie das Folgende schreibt – weder versehentlich noch aus anderen Gründen:
ComPtr<IHen> hen;
hen->AddRef();
Die Möglichkeit zum Aufrufen der virtuellen Funktionen "AddRef" oder "Release" sollte ausschließlich in die Zuständigkeit des intelligenten Zeigers fallen. Natürlich muss der intelligente Zeiger immer noch den verbleibenden Methoden gestatten, über solch einen Dereferenzierungsvorgang aufgerufen zu werden. Normalerweise würde der Dereferenzierungsoperator eines intelligenten Zeigers ungefähr wie folgt aussehen:
Interface * operator->() const noexcept
{
return m_ptr;
}
Das funktioniert für COM-Schnittstellenzeiger, und es gibt keine Notwendigkeit für eine Assertion, weil eine Zugriffsverletzung aussagekräftiger ist. Diese Implementierung gestattet es einem Aufrufer aber immer noch, "AddRef" und "Release" aufzurufen. Die Lösung besteht einfach darin, zu einem Typ zurückzukehren, der das Aufrufen von "AddRef" und "Release" unterbindet. Hier ist eine kleine Klassenvorlage ganz praktisch:
template <typename Interface>
class RemoveAddRefRelease : public Interface
{
ULONG __stdcall AddRef();
ULONG __stdcall Release();
};
Die RemoveAddRefRelease-Klassenvorlage erbt alle Methoden des Vorlagenarguments, deklariert "AddRef" und "Release" aber privat, sodass ein Aufrufer nicht versehentlich auf diese Methoden verweisen kann. Der Dereferenzierungsoperator des intelligenten Zeigers kann einfach "static_cast" verwenden, um den zurückgegebenen Schnittstellenzeiger zu schützen:
RemoveAddRefRelease<Interface> * operator->() const noexcept
{
return static_cast<RemoveAddRefRelease<Interface> *>(m_ptr);
}
Dies ist nur ein Beispiel dafür, wo mein ComPtr vom WRL-Ansatz abweicht. WRL macht alle Methoden von "IUnknown" privat, einschließlich "QueryInterface", während ich keinen Grund sehen, die Aufrufer auf diese Weise einzuschränken. Das bedeutet, dass WRL unausweichlich Alternativen für diesen wesentlichen Dienst bereitstellen muss, was eine erhöhte Komplexität und Verwirrung bei den Aufrufern nach sich zieht.
Da mein ComPtr entschieden das Kommando über die Verweiszählung übernimmt, sollte er diese besser auch korrekt durchführen. Nun, ich fange mit einem Paar privater Hilfsfunktionen an, beginnend mit einer für "AddRef":
void InternalAddRef() const noexcept
{
if (m_ptr)
{
m_ptr->AddRef();
}
}
Das ist jetzt alles nicht besonders aufregend, aber es gibt eine Reihe unterschiedlicher Funktionen, die einen bedingt hergestellten Verweis erfordern, wodurch sichergestellt wird, dass jederzeit das Richtige erfolgt. Die entsprechende Hilfsfunktion für "Release" ist ein wenig subtiler:
void InternalRelease() noexcept
{
Interface * temp = m_ptr;
if (temp)
{
m_ptr = nullptr;
temp->Release();
}
}
Warum temporär? Betrachten Sie die stärker intuitive, aber falsche Implementierung, die ungefähr dem entspricht, was ich (korrekt) gemacht habe, innerhalb der InternalAddRef-Funktion:
if (m_ptr)
{
m_ptr->Release(); // BUG!
m_ptr = nullptr;
}
Das Problem ist hier, dass das Aufrufen der Release-Methode eine ganze Kette von Ereignissen auslösen kann, durch die das Objekt ein zweites Mal freigegeben wird. Dieser zweite Lauf durch "InternalRelease" würde wieder einen Schnittstellenzeiger vorfinden, der nicht null ist, und erneut versuchen, ihn freizugeben (Release). Dies ist zugegebenermaßen ein ungewöhnliches Szenario, aber die Aufgabe des Bibliotheksentwicklers ist es, solche Aspekte zu berücksichtigen. Die ursprüngliche Implementierung unter Verwendung einer temporären Lösung vermeidet diesen doppelten Release-Aufruf, indem zuerst der Schnittstellenzeiger vom intelligenten Zeiger getrennt und dann nur "Release" aufgerufen wird. Wenn wir die vergangene Entwicklung betrachten, scheint es, als ob Jim Springfield der Erste war, der diesen ärgerlichen Fehler in ATL gefunden hat. Jedenfalls kann ich mit diesen beiden Hilfsfunktionen nun damit beginnen, einige der speziellen Memberfunktionen zu implementieren, die dabei helfen, dass sich das resultierende Objekt verhält und aussieht wie ein integriertes Objekt. Der Kopierkonstruktor ist ein einfaches Beispiel.
Im Gegensatz zu intelligenten Zeigern, die exklusiven Besitz gewähren, sollte intelligenten COM-Zeigern die Kopierkonstruktion gestattet sein. Sie müssen sorgfältig vorgehen, um Kopien um jeden Preis zu vermeiden, doch wenn eine Aufrufer wirklich eine Kopie möchte, dann erhält er auch eine. Hier ist ein einfacher Kopierkonstruktor:
ComPtr(ComPtr const & other) noexcept :
m_ptr(other.m_ptr)
{
InternalAddRef();
}
Hiermit wird er offensichtliche Fall einer Kopierkonstruktion abgedeckt. Der Schnittstellenzeiger wird kopiert, bevor die InternalAddRef-Hilfsfunktion aufgerufen wird. Wenn ich es dabei beließe, würde sich das Kopieren eines ComPtr höchstwahrscheinlich wie ein integrierte Zeiger anfühlen, aber eben nicht ganz. Ich könnte beispielsweise wie folgt eine Kopie erstellen:
ComPtr<IHen> hen;
ComPtr<IHen> another = hen;
Dies gibt wieder, was ich mit reinen Zeigern erreichen kann:
IHen * hen = nullptr;
IHen * another = hen;
Aber reine Zeiger lassen auch Folgendes zu:
IUnknown * unknown = hen;
Mit meinem einfachen Kopierkonstruktor darf ich nicht dasselbe machen wie mit ComPtr:
ComPtr<IUnknown> unknown = hen;
Auch wenn "IHen" letztendlich von "IUnknown" abgeleitet werden muss, wird "ComPtr<IHen>" nicht von "ComPtr<IUnknown>" abgeleitet, und der Compiler betrachtet sie als nicht verwandte Typen. Was ich brauche, ist ein Konstruktor, der als logischer Kopierkonstruktor für andere logisch verwandte ComPtr-Objekte fungiert, insbesondere jeden ComPtr mit einem Vorlagenargument, in das Vorlagenargument des konstruierten ComPtr konvertierbar ist. Hier verwendet WRL Typeigenschaften, was aber nicht wirklich notwendig ist. Ich benötige lediglich eine Funktionsvorlage, um eine eventuelle Konvertierung zu ermöglichen, und dann lasse ich den Compiler einfach überprüfen, ob tatsächlich Konvertierbarkeit vorliegt:
template <typename T>
ComPtr(ComPtr<T> const & other) noexcept :
m_ptr(other.m_ptr)
{
InternalAddRef();
}
Der Compiler überprüft, ob die Kopie tatsächlich sinnvoll ist, wenn der andere Zeiger verwendet wird, um den Schnittstellenzeiger des Objekts zu initialisieren. Folgendes lässt sich also kompilieren:
ComPtr<IHen> hen;
ComPtr<IUnknown> unknown = hen;
Das hier aber nicht:
ComPtr<IUnknown> unknown;
ComPtr<IHen> hen = unknown;
Und so sollte es sein. Natürlich betrachtet der Compiler die beiden immer noch als sehr unterschiedliche Typen, sodass die Konstruktorvorlage keinen wirklichen Zugriff auf die private Membervariable des anderen haben wird, es sei denn ich bringe sie einander näher:
template <typename T>
friend class ComPtr;
Sie könnten versucht sein, Teile des redundanten Codes zu entfernen, weil "IHen" in "IHen" konvertierbar ist. Warum also nicht einfach den tatsächlichen Kopierkonstruktor entfernen? Das Problem hierbei ist, dass dieser zweite Konstruktor nicht als Kopierkonstruktor vom Compiler betrachtet wird. Wenn Sie den Kopierkonstruktor auslassen, nimmt der Compiler an, dass Sie ihn entfernen wollten, und widerspricht jedem Verweis auf diese gelöschte Funktion. Weiter geht's.
Nachdem wir uns um die Kopierkonstruktion gekümmert haben, ist es sehr wichtig, dass ComPtr auch eine Verschiebekonstruktion bereitstellt. Wenn in einem gegebenen Szenario ein Verschieben zulässig ist, sollte ComPtr dem Compiler gestatten, dies zuzulassen, da es eine Verweisfestlegung spart, was im Vergleich zu einem Verschiebevorgang wesentlich aufwändiger wäre. Ein Verschiebekonstruktor ist sogar noch einfacher als ein Kopierkonstruktor, weil "InternalAddRef" nicht aufgerufen werden muss:
ComPtr(ComPtr && other) noexcept :
m_ptr(other.m_ptr)
{
other.m_ptr = nullptr;
}
Er kopiert den Schnittstellenzeiger, bevor der Zeiger im rvalue-Verweis gelöscht oder zurückgesetzt wird bzw. das Objekt, aus dem verschoben wird. In diesem Fall ist der Compiler jedoch nicht so wählerisch, und Sie können einfach zugunsten einer generischen Version, die konvertierbare Typen unterstützt, auf diesen Verschiebekonstruktor verzichten:
template <typename T>
ComPtr(ComPtr<T> && other) noexcept :
m_ptr(other.m_ptr)
{
other.m_ptr = nullptr;
}
Und damit sind die ComPtr-Konstruktoren erledigt. Der Desktruktor ist entsprechend vorhersehbar einfach:
~ComPtr() noexcept
{
InternalRelease();
}
Um die Feinheiten der Destruktion habe ich mich bereits innerhalb der InternalRelease-Hilfsfunktion gekümmert, sodass ich diese Funktion hier einfach zu meinem Vorteil wieder verwenden kann. Ich habe zwar die Kopier- und Verschiebekonstruktion diskutiert, aber die entsprechenden Zuweisungsoperatoren müssen diesem intelligenten Zeiger ebenfalls bereitgestellt werden, damit er sich wie ein echter Zeiger fühlt. Um diese Vorgänge zu unterstützen, werde ich ein weiteres Paar private Hilfsfunktionen hinzufügen. Die erste dient dem sicheren Abrufen einer Kopie eines gegebenen Schnittstellenzeigers:
void InternalCopy(Interface * other) noexcept
{
if (m_ptr != other)
{
InternalRelease();
m_ptr = other;
InternalAddRef();
}
}
Angenommen, die Schnittstellenzeiger sind nicht gleich (oder nicht beide Nullzeiger), dann gibt die Funktion alle vorhandenen Verweise frei, bevor eine Kopie des Zeigers erstellt und ein Verweis auf den neuen Schnittstellenzeiger gesichert wird. Auf diese Weise kann ich problemlos "InternalCopy" aufrufen, um den Besitz eines eindeutigen Verweises auf die gegebene Schnittstelle zu übernehmen, selbst dann, wenn der intelligente Zeiger bereits einen Verweis enthält. Ähnlich behandelt die zweite Hilfsfunktion das sichere Verschieben eines gegebenen Schnittstellenzeigers zusammen mit der von ihm dargestellten Verweiszählung:
template <typename T>
void InternalMove(ComPtr<T> & other) noexcept
{
if (m_ptr != other.m_ptr)
{
InternalRelease();
m_ptr = other.m_ptr;
other.m_ptr = nullptr;
}
}
Während "InternalCopy" von Natur aus konvertierbare Typen unterstützt, ist diese Funktion eine Vorlage für die Bereitstellung dieser Funktion für die Klassenvorlage. Ansonsten ist "InternalMove" im Großen und Ganzen dasselbe, verschiebt aber den Schnittstellenzeiger eher logisch, statt einen zusätzlichen Verweis zu erstellen. Nachdem wir das erledigt haben, lassen sich die Zuweisungsoperatoren recht einfach implementieren. Zuerst die Kopierzuweisung, und wie beim Kopierkonstruktor muss ich die kanonische Form angeben:
ComPtr & operator=(ComPtr const & other) noexcept
{
InternalCopy(other.m_ptr);
return *this;
}
Dann kann ich einen Vorlage für konvertierbare Typen bereitstellen:
template <typename T>
ComPtr & operator=(ComPtr<T> const & other) noexcept
{
InternalCopy(other.m_ptr);
return *this;
}
Aber wie beim Verschiebekonstruktor kann ich einfach eine einzige, generische Version der Verschiebezuweisung bereitstellen:
template <typename T>
ComPtr & operator=(ComPtr<T> && other) noexcept
{
InternalMove(other);
return *this;
}
Zwar sind Verschiebesemantiken dem Kopieren häufig überlegen, wenn es um verweiszählende intelligente Zähler geht, doch sind Verschiebevorgänge nicht ohne Kosten, und in manchen wichtigen Szenarios lassen sich Verschiebevorgänge sehr gut vermeiden, indem Tauschsemantiken (swap) bereitgestellt werden. Viele Containertypen bevorzugen Tauschvorgänge gegenüber Verschiebevorgängen, wodurch die Konstruktion einer großen Menge temporärer Objekte vermieden werden kann. Die Implementierung einer Tauschfunktionalität für ComPtr ist recht einfach:
void Swap(ComPtr & other) noexcept
{
Interface * temp = m_ptr;
m_ptr = other.m_ptr;
other.m_ptr = temp;
}
Ich würde den Standardtauschalgorithmus verwenden, aber zumindest in der Visual C++-Implementierung schließt der erforderliche <utility>-Header auch indirekt <stdio.h> ein, und ich möchte Entwickler nicht unbedingt dazu zwingen, das alles nur für einen Tauschvorgang einzuschließen. Natürlich muss ich, damit generische Algorithmen meine swap-Methode finden können, eine Nicht-Member-Tauschfunktion (Kleinbuchstaben) bereitstellen:
template <typename Interface>
void swap(ComPtr<Interface> & left,
ComPtr<Interface> & right) noexcept
{
left.Swap(right);
}
Solange dies im selben Namespace wie die ComPtr-Klassenvorlage definiert ist, wird der Compiler problemlos generischen Algorithmen die Verwendung von "swap" gestatten.
Eine weitere nette Funktion von C++11 sind explizite Konvertierungsoperatoren. Historisch gesehen bedurfte es einiger umständlicher Hacks, um einen zuverlässigen, expliziten booleschen Operator zu erzeugen, mit dem überprüft werden kann, ob ein intelligenter Zeiger logisch nicht null ist. Heutzutage ist das so einfach wie folgt:
explicit operator bool() const noexcept
{
return nullptr != m_ptr;
}
Und damit sind die speziellen und quasi-speziellen Member versorgt, durch die sich mein intelligenter Zeiger fast wie ein integrierter Typ verhält, mit so viel Unterstützung, wie ich bereitstellen kann, um dem Compiler bei der Optimierung und damit verbundenen Beseitigung jeglichen Zusatzaufwands zu helfen. Was noch bleibt, ist eine kleine Auswahl von Hilfsfunktionen, die in vielen Fällen für COM-Anwendungen erforderlich sind. An dieser Stelle sollte man Sorgfalt walten lassen, um das Hinzufügen von zu vielen Sonderfunktionen zu vermeiden. Dennoch gibt es immer noch eine Handvoll Funktionen, die von praktisch jeder nicht trivialen Anwendung oder Komponente verwendet werden. Zuerst muss der zugrunde liegende Verweis explizit freigegeben werden. Das ist recht einfach:
void Reset() noexcept
{
InternalRelease();
}
Und dann benötigen wir eine Methode, um den zugrunde liegenden Zeiger abzurufen, falls der Aufrufer ihn als Argument an eine andere Funktion übergeben muss:
Interface * Get() const noexcept
{
return m_ptr;
}
Eventuell muss ich den Verweis trennen, um ihn vielleicht an den Aufrufer zurückzugeben:
Interface * Detach() noexcept
{
Interface * temp = m_ptr;
m_ptr = nullptr;
return temp;
}
Möglicherweise muss ich eine Kopie eines bestehenden Zeigers erstellen. Dies könnte ein dem Aufrufer gehörender Verweis sein, den ich behalten möchte:
void Copy(Interface * other) noexcept
{
InternalCopy(other);
}
Oder ich habe eventuell einen reinen Zeiger, der Besitzer eines Verweises auf sein Ziel ist, den ich gerne anfügen möchten, ohne dass ein zusätzlicher Verweis eingerichtet wird. Dies kann in seltenen Fällen auch beim Verbinden von Verweisen nützlich sein:
void Attach(Interface * other) noexcept
{
InternalRelease();
m_ptr = other;
}
Die restlichen paar Funktionen spielen eine besonders kritische Rolle, weshalb ich mich ein wenig länger mit ihnen beschäftigen möchte. COM-Methoden geben herkömmlicherweise Verweise als Ausgabeparameter mittels eines Zeigers auf einen Zeiger zurück. Es ist wichtig, dass alle intelligenten COM-Zeiger eine Möglichkeit bieten, um solche Verweise direkt zu erfassen. Hierfür stelle ich die GetAddressOf-Methode bereit:
Interface ** GetAddressOf() noexcept
{
ASSERT(m_ptr == nullptr);
return &m_ptr;
}
Dies ist wieder ein Punkt, in dem sich mein ComPtr von der WRL-Implementierung auf subtile aber entscheidende Weise unterscheidet. Beachten Sie, dass "GetAddressOf" bestätigt, dass es keinen Verweis besitzt, bevor es seine Adresse zurückgibt. Dies ist von essenzieller Bedeutung, da die aufgerufene Funktion sonst einfach jeden Verweis überschreibt, der eventuell vorhanden war, wodurch Sie sich selbst eine Verweislücke einhandeln. Ohne die Assertion lassen sich solche Fehler wesentlich schwerer auffinden. Am anderen Ende des Spektrums liegt die Fähigkeit zum Ausgeben von Verweisen – entweder desselben Typs oder für andere Schnittstellen, die vom zugrunde liegenden Objekt eventuell implementiert werden. Wenn ein weiterer Verweis auf dieselbe Schnittstelle gewünscht wird, kann ich das Aufrufen von "QueryInterface" vermeiden und einfach mithilfe der von COM vorgeschriebenen Konvention einen zusätzlichen Verweis zurückgeben:
void CopyTo(Interface ** other) const noexcept
{
InternalAddRef();
*other = m_ptr;
}
Und Sie könnten es wie folgt verwenden:
hen.CopyTo(copy.GetAddressOf());
Andernfalls kann "QueryInterface" selbst ohne weitere Hilfe von ComPtr benutzt werden:
HRESULT hr = hen->QueryInterface(other.GetAddressOf());
Dies basiert tatsächlich auf einer Funktionsvorlage, die direkt von "IUnknown" bereitgestellt wird, um zu vermeiden, dass die GUID der Schnittstelle explizit angegeben werden muss.
Schließlich gibt es häufig Fälle, in denen eine App oder Komponente eine Schnittstelle abfragen muss, ohne sie notwendigerweise an den Aufrufer in der klassischen COM-Konvention zurückzugeben. In solchen Fällen ist es sinnvoller, diesen neuen Schnittstellenzeiger wie folgt gut verpackt in einem weiteren ComPtr zurückzugeben:
template <typename T>
ComPtr<T> As() const noexcept
{
ComPtr<T> temp;
m_ptr->QueryInterface(temp.GetAddressOf());
return temp;
}
Ich kann dann einfach den expliziten booleschen Operator verwenden, um zu überprüfen, ob die Abfrage erfolgreich war. Zu guter Letzt stellt ComPtr aus Gründen der Bequemlichkeit und zur Unterstützung verschiedener Container und generischer Algorithmen auch alle erwarteten Nicht-Member-Vergleichsoperatoren zur Verfügung. Dies hilft wiederum lediglich dabei, dass sich der intelligente Zeiger wie ein integrierter Zeiger verhält und aussieht, während die wesentlichen Dienste für die ordnungsgemäße Verwaltung der Ressource sowie die notwendigen Dienste bereitgestellt werden, die von COM-Apps und -Komponenten erwartet werden. Die ComPtr-Klassenvorlage ist lediglich ein weiteres Beispiel aus "Modernes C++ für die Windows-Runtime" (moderncpp.com).
Kenny Kerr ist Programmierer aus Kanada sowie Autor bei Pluralsight und Microsoft MVP. Er veröffentlicht Blogs unter kennykerr.ca, und Sie können ihm auf Twitter unter twitter.com/kennykerr folgen.
Unser Dank gilt dem folgenden technischen Experten von Microsoft für die Durchsicht dieses Artikels: James McNellis