C++

Visual C++ 2015 bringt modernes C++ und die Windows-API zusammen

Kenny Kerr

Visual C++ 2015 ist der Höhepunkt einer großen Anstrengung des C++-Teams, modernes C++ auf die Windows-Plattform zu bringen. Mit den letzten Versionen wurde Visual C++ beständig um eine reiche Auswahl an modernen C++-Sprach- und Bibliotheksfunktionen erweitert, die zusammen genommen eine herausragende Umgebung bieten, in der sich universelle Windows-Apps und -Komponenten erstellen lassen. Visual C++ 2015 baut auf dem bemerkenswerten Fortschritt auf, der mit diesen früheren Versionen eingeführt wurde, und bietet einen ausgereiften Compiler, der viele Funktionen von C++11 und eine Teilmenge der Funktionen von C++ 2015 unterstützt. Über den Grad von Vollständigkeit könnte man wohl diskutieren, aber ich glaube, es ist nur fair zu sagen, dass der Compiler die meisten wichtigen Sprachfunktionen unterstützt, was es modernem C++ ermöglicht, eine neue Epoche der Bibliotheksentwicklung für Windows einzuleiten. Und das ist wirklich das Entscheidende. Solange der Compiler die Entwicklung effizienter und eleganter Bibliotheken unterstützt, können Entwickler damit fortfahren, ausgezeichnete Apps und Komponenten zu entwickeln.

Statt Ihnen nur eine langweilige Liste der neuen Funktionen zu präsentieren oder eine schnelle Übersichtstour der Funktionen zu bieten, werden ich mit Ihnen schrittweise die Entwicklung eines herkömmlich komplexen Stücks Code durchgehen, der sich jetzt, ganz offen gesagt, ziemlich angenehm schreiben lässt – dank der Ausgereiftheit des Visual C++-Compilers. Ich werde Ihnen etwas zeigen, das ein wesentlicher Teil von Windows ist und im Kern praktisch jeder signifikanten aktuellen und zukünftigen API liegt.

Es ist schon ein bisschen ironisch, dass C++ schließlich doch noch modern genug geworden ist für COM. Ja genau, ich spreche vom "Component Object Model", das schon seit Jahren die Grundlage für viele Windows-APIs bildet und auch weiterhin die Basis der Windows-Runtime darstellt. Zwar ist COM hinsichtlich seines ursprünglichen Designs unzweifelhaft an C++ gebunden, wobei es einige Anleihen bei den binären und semantischen Konventionen von C++ macht, doch ist es in seiner Gesamtheit nie elegant gewesen. Auf Teile von C++, die als nicht portierbar genug galten, z. B. "dynamic_cast", musste verzichtet werden zugunsten von portierbaren Lösungen, durch die die Entwicklung von C++-Implementierungen zu einer immer größeren Herausforderung wurde. Viele Lösungen wurden im Laufe der Jahre bereitgestellt, um C++-Entwicklern COM schmackhafter zu machen. Die C++/CX-Spracherweiterung ist vielleicht das bisher ehrgeizigste Projekt des Visual C++-Teams. Ironischerweise haben diese Bemühungen, die Unterstützung von Standard-C++ zu verbessern, C++/CX weit übertroffen und machen somit eine Spracherweiterung überflüssig.

Um diesen Punkt zu beweisen, werde ich Ihnen zeigen, wie Sie "IUnknown" und "IInspectable" vollständig in modernem C++ implementieren. Die beiden sind weder modern noch besonders attraktiv. "IUnknown" ist weiterhin die zentrale Abstraktion für wichtige APIs wie DirectX. Und diese Schnittstellen, wobei "IInspectable" von "IUnknown" abgeleitet wird, befinden sich im Herzen der Windows-Runtime. Ich werden Ihnen demonstrieren, wie Sie sie ohne Spracherweiterungen, Schnittstellentabellen oder andere Makros implementieren – einfach effizientes und elegantes C++ mit Mengen von umfangreichen Typinformationen, mit deren Hilfe Compiler und Entwickler ausführlich aushandeln können, was erzeugt werden muss.

Die größte Herausforderung hierbei ist es, eine Möglichkeit zu finden, um die Liste der Schnittstellen zu beschreiben, die eine COM- oder Windows-Runtime-Klasse zu implementieren beabsichtigt, und diese Beschreibung so zu gestalten, dass Sie für den Entwickler bequem nutzbar, aber auch gleichzeitig für den Compiler zugänglich ist. Insbesondere muss diese Typenliste so verfügbar gemacht werden, dass der Compiler die Schnittstellen abfragen und sogar aufzählen kann. Wenn ich das hinbekommen, könnte ich in der Lage sein, den Compiler so weit zu bekommen, dass er den Code für die IUnknown-Methode "QueryInterface" und optional auch die IInspectable-Methode "GetIids" generiert. Es sind nämlich diese beiden Methoden, die die größte Herausforderung darstellen. Traditionellerweise haben die einzigen Lösungen bisher Spracherweiterungen, furchtbare Makros oder eine Menge praktisch nicht zu pflegenden Code verwendet.

Beide Methodenimplementierungen erfordern eine Liste der Schnittstellen, die eine Klasse zu implementieren beabsichtigt. Die logische Wahl zum Beschreiben einer solchen Liste von Typen ist eine variadic-Vorlage:

template <typename ... Interfaces>
class __declspec(novtable) Implements : public Interfaces ...
{
};

Das erweiterte novtable-Attribut "__declspec" bewahrt alle Konstruktoren und Destruktoren vor der Notwendigkeit, die vfptr in so abstrakten Klassen initialisieren zu müssen, was häufig eine signifikante Verringerung der Codemenge bedeutet. Die Implements-Klassenvorlage enthält ein Vorlagenparameterpaket, was sie zu einer variadic-Vorlage wird. Ein Parameterpaket ist ein Vorlagenparameter, der eine beliebige Anzahl von Vorlagenargumenten akzeptiert. Der Trick hierbei ist, dass Parameterpakte normalerweise verwendet werden, um Funktionen das Akzeptieren einer beliebigen Anzahl von Argumenten zu gestatten, doch in diesem Fall beschreibe ich eine Vorlage, deren Argumente ausschließlich zum Zeitpunkt der Kompilierung abgefragt werden. Die Schnittstellen tauchen nie in einer Funktionsparameterliste auf.

Eine Verwendung für diese Argumente ist bereits offensichtlich. Das Parameterpaket wird erweitert, um die Liste der öffentlichen Basisklassen zu bilden. Natürlich bin ich immer noch dafür verantwortlich, diese virtuellen Funktionen tatsächlich zu implementieren, aber zu diesem Zeitpunkt kann ich eine konkrete Klasse beschreiben, die eine beliebige Anzahl von Schnittstellen implementiert:

class Hen : public Implements<IHen, IHen2>
{
};

Da das Parameterpaket erweitert ist, um die Liste der Basisklassen festzulegen, entspricht es dem, was ich sonst selbst geschrieben hätte, wie folgt:

class Hen : public IHen, public IHen2
{
};

Das Schöne an der Strukturierung der Implements-Klassenvorlage in dieser Art ist, dass ich jetzt die Implementierung von unterschiedlichem Startseitencode in die "Implements"-Klassenvorlage einfügen kann, während der Entwickler der Hen-Klasse diese unauffällige Abstraktion verwenden kann, ohne dabei die zugrunde liegende Magie beachten zu müssen.

So weit so gut. Jetzt kümmere ich mich um die Implementierung von "IUnknown" selbst. Angesichts des Informationstyps, den der Compiler nun zur Verfügung hat, sollte ich in der Lage sein, sie vollständig innerhalb der Implements-Klassenvorlage zu implementieren. "IUnknown" stellt zwei Möglichkeiten bereit, die für COM-Klassen so essenziell sind wie Sauerstoff und Wasser für Menschen. Die erste und vielleicht einfachere der beiden ist "Verweiszählung", wobei es sich um die Methode handelt, mit der COM-Objekte ihre Gültigkeitsdauer verfolgen. COM schreibt eine Form der intrusiven Verweiszählung vor, wobei jedes Objekt dafür verantwortlich ist, seine eigene Gültigkeitsdauer zu verwalten, basierend auf seiner Erfassung der Anzahl noch ausstehender vorhandener Verweise. Dies steht im Gegensatz zur einem intelligenten Verweiszählungszeiger wie der C++11-Klassenvorlage "shared_ptr", bei der das Objekt keine Kenntnisse davon hat, in wessen gemeinsamem Besitz es ist. Wir könnten nun über die Vor- und Nachteile dieser beiden Ansätze diskutieren, aber in der Praxis ist der COM-Ansatz häufig effizienter, und außerdem ist dies eben die Art, in der COM funktioniert, und somit müssen Sie sich damit arrangieren. Aber vielleicht können wir uns ja zumindest darauf einigen, dass es eine schreckliche Idee ist, eine COM-Schnittstelle in eine "shared_ptr" zu wrappen!

Ich beginne mit dem einzigen Runtime-Overhead, der von der Implements-Klassenvorlage eingeführt wird:

protected:
  unsigned long m_references = 1;
  Implements() noexcept = default;
  virtual ~Implements() noexcept
  {}

Der Standardkonstruktor selbst ist kein wirklicher Overhead. Er stellt einfach nur sicher, dass der resultierende Konstruktor, der die Verweiszählung initialisieren soll, geschützt und nicht öffentlich ist. Sowohl die Verweiszählung als auch der virtuelle Destruktor sind geschützt. Wird die Verweiszählung abgeleiteten Klassen zugänglich gemacht, lassen sich komplexere Klassen zusammensetzen. Die meisten Klassen können dies einfach ignorieren, aber beachten Sie bitte, dass ich die Verweiszählung auf 1 initialisiere. Dies widerspricht der allgemeinen Überzeugung, dass die Verweiszählung anfangs 0 sein sollte, weil noch keine Verweise ausgegeben wurden. Dieser Ansatz wurde von ATL populär gemacht und zweifellos von "Essential COM", von Don Box, beeinflusst, aber er ist recht problematisch, wie sich durch eine Untersuchung des ATL-Quellcodes gut nachweisen lässt. Beginnend mit der Annahme, dass der Besitz des Verweises sofort von einem Aufrufer übernommen oder an einen intelligenten Zeiger angefügt wird, sorgt für einen weit weniger fehleranfälligen Konstruktionsprozess.

Ein virtueller Destruktor ist insofern eine sehr bequeme Lösung, als dass er es der Implements-Klassenvorlage gestattet, die Verweiszählung zu implementieren, statt die konkrete Klasse zu zwingen, die Implementierung bereitzustellen. Eine andere Möglichkeit wäre die Verwendung des "Curiously Recurring Template Pattern" (CRTP), um die virtuelle Funktion zu vermeiden. Normalerweise würde ich solch einen Ansatz bevorzugen, aber er würde die Abstraktion ein wenig verkomplizieren, und da eine COM-Klasse von Natur aus eine vtable besitzt, gibt es keinen zwingenden Grund, um hier eine virtuelle Funktion zu vermeiden. Mit diesen vorhandenen Primitiven ist es nun ein Leichtes, sowohl "AddRef" als auch "Release" innerhalb der Implements-Klassenvorlage zu implementieren. Zunächst kann die AddRef-Methode einfach das interne "InterlockedIncrement" verwenden, um die Verweiszählung zu erhöhen:

virtual unsigned long __stdcall AddRef() noexcept override
{
  return InterlockedIncrement(&m_references);
}

Das bedarf keiner weiteren Erklärung. Geraten Sie nicht in Versuchung, ein komplexes Schema zu entwerfen, mit dem Sie die internen Funktionen "InterlockedIncrement" und "InterlockedDecrement" bedingt durch die Inkrement- und Dekrement-Operatoren von C++ ersetzen könnten. ATL versucht genau das um den Preis einer hohen Komplexität. Wenn es Ihnen um Effizienz geht, verwenden Sie Ihre Bemühungen lieber auf die Vermeidung vermeidbarer Aufrufe von "AddRef" und "Release". Noch einmal, modernes C++ rettet Sie seiner Unterstützung für Verschiebesemantiken und seiner Fähigkeit zum Verschieben des Besitzes von Verweisen ohne Verweiskonflikte. Nun, die Release-Methode ist nur unwesentlich komplexer:

virtual unsigned long __stdcall Release() noexcept override
{
  unsigned long const remaining = InterlockedDecrement(&m_references);
  if (0 == remaining)
  {
    delete this;
  }
  return remaining;
}

Die Verweiszählung wird verringert, und das Ergebnis wird einer lokalen Variablen zugewiesen. Dies ist wichtig, da dieses Ergebnis zurückgegeben werden sollte, aber wenn das Objekt zerstört werden soll, wäre es unzulässig, auf die Membervariable zu verweisen. Vorausgesetzt, es gibt keine ausstehenden Verweise, wird das Objekt einfach über einen Aufruf des zuvor erwähnten virtuellen Destruktors gelöscht. Hiermit wird die Verweiszählung beendet, und die konkrete Hen-Klasse ist immer noch so einfach wie zuvor:

class Hen : public Implements<IHen, IHen2>
{
};

Nun ist es an der Zeit, sich mit der wundervollen Welt von "QueryInterface" zu beschäftigen. Das Implementieren dieser "IUnknown"-Methode ist ein nicht ganz triviales Unterfangen. Ich behandle dieses Thema ausführlich in meinen Pluralsight-Kursen, und Sie können über die mannigfaltigen Weisen zum Bereitstellen Ihrer eigenen Implementierung in "Essential COM" (Addison-Wesley Professional, 1998), von Don Box, nachlesen. Doch seien Sie gewarnt, dass dieser ganz ausgezeichnete Text über COM auf C++98 basiert und gar nichts mit modernem C++ zu tun hat. Aus Gründen der Platz- und Zeitersparnis setze ich voraus, dass Sie in gewissem Maße mit der Implementierung von QueryInterface vertraut sind, und konzentriere mich stattdessen darauf, wie die Implementierung in modernem C++ erfolgt. Hier sehen Sie die virtuelle Methode selbst:

virtual HRESULT __stdcall QueryInterface(
  GUID const & id, void ** object) noexcept override
{
}

Bei einer gegebenen GUID, die eine bestimmte Schnittstelle identifiziert, sollte QueryInterface bestimmen, ob das Objekt die gewünschte Schnittstelle implementieret. Ist dies der Fall, muss die Verweiszählung für das Objekt erhöht und dann über den out-Parameter zum gewünschten Schnittstellenzeiger zurückgekehrt werden. Ist dies nicht der Fall, muss ein Nullzeiger zurückgegeben werden. Deshalb beginne ich mit einem groben Abriss:

*object = // Find interface somehow
if (nullptr == *object)
{
  return E_NOINTERFACE;
}
static_cast<::IUnknown *>(*object)->AddRef();
return S_OK;

QueryInterface versucht also zuerst, irgendwie die gewünschte Schnittstelle zu finden. Wenn die Schnittstelle nicht unterstützt wird, wird der erforderliche Fehlercode "E_NO­INTERFACE" zurückgegeben. Beachten Sie, wie ich mich bereits um die Voraussetzung gekümmert habe, dass der resultierende Schnittstellenzeiger bei einem Fehler gelöscht wird. Sie sollten sich QueryInterface am ehesten als binären Vorgang vorstellen. Entweder findet es die gewünschte Schnittstelle oder nicht. Geraten Sie nicht in Versuchung, hier kreativ zu werden, und nur bedingt positiv zu reagieren. Obgleich es einige begrenzte Optionen gibt, die in der COM-Spezifikation zugelassen sind, setzen die meisten Consumer schlicht voraus, dass die Schnittstelle nicht unterstützt wird, egal, welchen Fehlercode Sie zurückgeben. Jeder Fehler in Ihrer Implementierung wird Sie zweifellos Debuggingorgien ohne Ende kosten. QueryInterface ist zu grundlegend, um damit herumzuspielen. Schließlich wird erneut "AddRef" durch den resultierenden Schnittstellenzeiger aufgerufen, um einige seltene aber zulässige Klassenzusammensetzungsszenarios zu unterstützen. Diese werden nicht explizit von der Implements-Klassenvorlage unterstützt, aber ich möchte hier lieber ein gutes Vorbild abgeben. Es ist wichtig, dass Sie beachten, dass die Verweiszählungsvorgänge eher schnittstellenspezifisch als objektspezifisch sind. Sie können nicht einfach "AddRef" oder "Release" an jeder Schnittstelle aufrufen, die zu einem Objekt gehört. Sie müssen die COM-Regeln einhalten, die die Objektidentität steuern, andernfalls riskieren Sie es, unzulässigen Code einzuführen, der unerklärlicherweise unterbricht.

Wie ermittle ich also, ob die angeforderte GUID eine Schnittstelle repräsentiert, die die Klasse zu implementieren beabsichtigt? Das ist die Stelle, an der ich zu den Typinformationen zurückkehren kann, die die Implements-Klassenvorlage über ihr Vorlagenparameterpaket sammelt. Denken Sie daran, dass mein Ziel ist, den Compiler die Implementierung für mich übernehmen zu lassen. Ich möchte, dass der resultierende Code so effizient ist, als ob ich ihn per Hand geschrieben hätte – oder sogar noch besser. Daher führe ich diese Abfrage mit einem Satz von variadic-Funktionsvorlagen durch – Funktionsvorlagen, die selbst Vorlagenparameterpakete enthalten. Ich beginne mit einer BaseQueryInterface-Funktionsvorlage, um den Prozess anzustoßen:

virtual HRESULT __stdcall QueryInterface(
  GUID const & id, void ** object) noexcept override
{
  *object = BaseQueryInterface<Interfaces ...>(id);

BaseQueryInterface ist im Wesentlichen eine moderne C++-Projektion von "IUnknown QueryInterface". Statt ein HRESULT zurückzugeben, gibt sie den Schnittstellenzeiger direkt zurück. Fehler werden offensichtlich durch einen Nullzeiger angezeigt. Es wird ein einziges Funktionsargument akzeptiert, nämlich die GUID, die die zu suchende Schnittstelle identifiziert. Noch wichtiger ist, dass ich das Parameterpaket der Klassenvorlage in seiner Gesamtheit erweitere, sodass die BaseQueryInterface-Funktion den Prozess der Aufzählung der Schnittstellen beginnen kann. Vielleicht haben Sie zuerst gedacht, dass BaseQueryInterface, da sie ein Member der Implements-Klassenvorlage ist, einfach direkt auf diese Liste von Schnittstellen zugreifen kann, aber ich muss dieser Funktion gestatten, die erste Schnittstelle aus der Liste zu verwenden, indem ich wie folgt vorgehe:

template <typename First, typename ... Rest>
void * BaseQueryInterface(GUID const & id) noexcept
{
}

Auf diese Weise kann BaseQueryInterface die erste Schnittstelle identifizieren und die restlichen für eine nachfolgende Suche dort belassen. Wie Sie sehen, hat COM eine Reihe spezifischer Regeln für die Unterstützung der Objektidentität, die QueryInterface implementieren oder zumindest berücksichtigen muss. Insbesondere Anforderungen von IUnknown müssen immer exakt denselben Zeiger zurückgeben, damit ein Client bestimmen kann, ob zwei Schnittstellenzeiger auf dasselbe Objekt verweisen. Somit ist die BaseQueryInterface-Funktion eine gute Stelle, um dort einige dieser Axiome zu implementieren. Ich könnte deshalb mit einem Vergleich der angeforderten GUID mit dem ersten Vorlagenargument beginnen, das die erste Schnittstelle darstellt, die die Klasse zu implementieren beabsichtigt. Wenn hier keine Übereinstimmung vorliegt, überprüfe ich, ob IUnknown angefordert wird:

if (id == __uuidof(First) || id == __uuidof(::IUnknown))
{
  return static_cast<First *>(this);
}

Vorausgesetzt, eins davon ist eine Übereinstimmung, dann gebe ich einfach den unzweideutigen Schnittstellenzeiger für die erste Schnittstelle zurück. Mit "static_cast" wird sichergestellt, dass sich der Compiler an der Doppeldeutigkeit mehrerer, auf IUnknown basierender Schnittstellen, nicht stört. Die Umwandlung passt den Zeiger lediglich so an, dass die richtige Stelle in der Klassen-vtable gefunden wird, und weil alle Schnittstellen-vtables mit drei Methoden von IUnknown beginnen, ist dies absolut zulässig.

Wo ich schon mal hier bin, könnte ich auch direkt noch optionale Unterstützung für IInspectable-Abfragen hinzufügen. IInspectable ist ein wenig sonderbar. In mancher Hinsicht ist es das IUnknown-Äquivalent der Windows-Runtime, weil jede Windows-Runtime-Schnittstelle, die in Sprachen wie C# und JavaScript projektiert wird, direkt von IInspectable abgeleitet werden muss statt lediglich von IUnknown alleine. Dies ist eine bedauerliche Realität, mit der die Art integriert wird, in der die Common Language Runtime Objekte und Schnittstellen implementiert, die im Gegensatz zur Funktionsweise von C++ steht sowie zur Art, in der COM traditionellerweise definiert wurde. Hieraus ergeben sich leider auch einige ziemlich unglückliche Auswirkungen auf die Leistung im Bereich der Objektzusammensetzung, doch dies ist ein umfangreiches Thema, das ich in einem der Folgeartikel behandeln werde. Was QueryInterface anbelangt, muss ich nur sicherstellen, dass IInspectable abgefragt werden kann, wenn es sich um eine Implementierung einer Windows-Runtime-Klasse statt einfach einer klassischen COM-Klasse handeln sollte. Obgleich die expliziten COM-Regeln bezüglich IUnknown nicht für IInspectable gelten, kann ich letztere hier einfach auf ziemlich dieselbe Weise behandeln. Dabei ergeben sich aber zwei Herausforderungen. Erstens muss ich ermitteln, ob eine der implementierten Schnittstellen von IInspectable abgeleitet ist. Und zweitens benötige ich den Typ einer solchen Schnittstelle, damit ich einen ordnungsgemäß angepassten Schnittstellenzeiger ohne Doppeldeutigkeit zurückgeben kann. Wenn ich voraussetzen könnte, dass die erste Schnittstelle in der Liste immer auf IInspectable basierte, könnte ich einfach BaseQueryInterface wie folgt aktualisieren:

if (id == __uuidof(First) ||
  id == __uuidof(::IUnknown) ||
  (std::is_base_of<::IInspectable, First>::value &&
  id == __uuidof(::IInspectable)))
{
  return static_cast<First *>(this);
}

Beachten Sie, dass ich die C++11-Typeigenschaft "is_base_of" verwende, um zu bestimmen, ob das erste Vorlagenargument eine von IInspectable abgeleitete Schnittstelle ist. Hierdurch wird sichergestellt, dass der folgende Vergleich vom Compiler ausgeschlossen wird, falls Sie eine klassische COM-Klasse ohne Unterstützung für die Windows-Runtime implementieren. Auf diese Weise kann ich reibungslos sowohl Windows-Runtime- als auch klassische COM-Klassen unterstützen, ohne zusätzliche syntaktische Komplexität für Komponentenentwickler und ohne jeglichen nicht notwendigen Runtime-Overhead zu erzeugen. Hier bleibt dann aber noch die Möglichkeit für einen kleinen Fehler, falls Sie als Erstes eine nicht von IInspectable abgeleitete Schnittstelle aufzählen. Wir müssen also "is_base_of" durch etwas ersetzen, das die gesamte Liste der Schnittstellen überprüfen kann:

template <typename First, typename ... Rest>
constexpr bool IsInspectable() noexcept
{
  return std::is_base_of<::IInspectable, First>::value ||
    IsInspectable<Rest ...>();
}

IsInspectable basiert immer noch auf der Typeigenschaft "is_base_of", wendet sie aber nun auf jede Schnittstelle an, bis eine Übereinstimmung gefunden wird. Wenn keine auf IInspectable basierenden Schnittstellen gefunden werden, wird die Abschlussfunktion aufgerufen:

template <int = 0>
constexpr bool IsInspectable() noexcept
{
  return false;
}

Ich wende mich gleich wieder dem seltsamen, namenlosen Standardargument zu. Angenommen, IsInspectable gibt "true" zurück, dann muss ich die erste auf IInspectable basierende Schnittstelle finden:

template <int = 0>
void * FindInspectable() noexcept
{
  return nullptr;
}
template <typename First, typename ... Rest>
void * FindInspectable() noexcept
{
  // Find somehow
}

Ich kann erneut auf die Typeigenschaft "is_base_of" bauen, aber diesmal einen tatsächlichen Schnittstellenzeiger zurückgeben, wenn eine Übereinstimmung gefunden wird:

#pragma warning(push)
#pragma warning(disable:4127) // conditional expression is constant
if (std::is_base_of<::IInspectable, First>::value)
{
  return static_cast<First *>(this);
}
#pragma warning(pop)
return FindInspectable<Rest ...>();

BaseQueryInterface kann dann einfach IsInspectable zusammen mit FindInspectable verwenden, um Abfragen für IInspectable zu unterstützen:

if (IsInspectable<Interfaces ...>() && 
  id == __uuidof(::IInspectable))
{
  return FindInspectable<Interfaces ...>();
}

Erneut wird die konkrete Hen-Klasse vorausgesetzt:

class Hen : public Implements<IHen, IHen2>
{
};

Die Implements-Klassenvorlage stellt sicher, dass der Compiler den effizientesten Code generiert, egal, ob IHen oder IHen2 von IInspectable oder einfach von IUnknown (oder einer anderen Schnittstelle) abgeleitet wird. Jetzt kann ich schließlich den rekursiven Teil von QueryInterface implementieren, um alle zusätzlichen Schnittstellen abzudecken, wie z. B. IHen2 im vorherigen Beispiel. BaseQueryInterface wird mit dem Aufrufen einer FindInterface-Funktionsvorlage beendet:

template <typename First, typename ... Rest>
void * BaseQueryInterface(GUID const & id) noexcept
{
  if (id == __uuidof(First) || id == __uuidof(::IUnknown))
  {
    return static_cast<First *>(this);
  }
  if (IsInspectable<Interfaces ...>() && 
    id == __uuidof(::IInspectable))
  {
    return FindInspectable<Interfaces ...>();
  }
  return FindInterface<Rest ...>(id);
}

Beachten Sie, dass ich diese FindInterface-Funktionsvorlage größtenteils auf dieselbe Weise aufrufe wie ursprünglich BaseQueryInterface. In diesem Fall übergebe ich die restlichen Schnittstellen an sie. Ich erweitere insbesondere das Parameterpaket so, dass es wieder die erste Schnittstelle in der restlichen Liste identifizieren kann. Dabei ergeben sich aber zwei Probleme. Da das Vorlagenparameterpaket nicht als Funktionsargumente erweitert ist, kann es in einer ärgerlichen Situation enden, in der mich die Sprache nicht ausdrücken lässt, was ich wirklich möchte. Doch mehr dazu gleich. Diese "rekursive" variadic-Vorlage von FindInterface ist, wie Sie erwartet haben:

template <typename First, typename ... Rest>
void * FindInterface(GUID const & id) noexcept
{
  if (id == __uuidof(First))
  {
    return static_cast<First *>(this);
  }
  return FindInterface<Rest ...>(id);
}

Sie trennt ihr erstes Vorlagenargument vom Rest und gibt den angepassten Schnittstellenzeiger zurück, wenn eine Übereinstimmung vorliegt. Andernfalls ruft sie sich selbst auf, bis die Schnittstellenliste abgearbeitet ist. Zwar bezeichne ich dies salopp als eine Rekursion während der Kompilierung, doch es ist wichtig, dass Sie beachten, dass diese Funktionsvorlage – sowie die anderen ähnlichen Beispiele in der Implements-Klassenvorlage – technisch gesehen nicht rekursiv sind, noch nicht einmal während der Kompilierung. Jede Instanziierung der Funktionsvorlage ruft eine andere Instanziierung der Funktionsvorlage auf. Beispielsweise ruft FindInterface<IHen, IHen2> FindInterface<IHen2> auf, was wiederum FindInterface<> aufruft. Damit sie rekursiv würde, müsste FindInterface<IHen, IHen2> FindInterface<IHen, IHen2> aufrufen, was nicht geschieht.

Bedenken Sie jedoch, dass diese "Rekursion" während der Kompilierung erfolgt, und es so ist, als ob Sie all diese If-Anweisungen eine nach der anderen per Hand geschrieben hätten. Dabei gibt es jedoch einen Haken. Wie wird diese Sequenz beendet? Natürlich wenn die Liste der Vorlagenargumente leer ist. Das Problem ist, dass in C++ bereits definiert ist, was eine leere Liste mit Vorlagenparametern bedeutet:

template <>
void * FindInterface(GUID const &) noexcept
{
  return nullptr;
}

Dies ist fast richtig, aber der Compiler teilt Ihnen mit, dass für diese Spezialisierung keine Funktionsvorlage vorhanden ist. Und doch, wenn ich diese Abschlussfunktion nicht bereitstelle, kann der Compiler den letzten Aufruf nicht kompilieren, wenn das Parameterpaket leer ist. Dies ist nicht der Fall beim Überladen von Funktionen, da die Liste der Argumente dieselbe bleibt. Glücklicherweise ist die Lösung hinreichend einfach. Ich kann vermeiden, dass die Abschlussfunktion wie eine Spezialisierung aussieht, indem ich ihr ein namenloses Standardargument übergebe:

template <int = 0>
void * FindInterface(GUID const &) noexcept
{
  return nullptr;
}

Der Compiler ist zufrieden, und wenn eine nicht unterstützte Schnittstelle angefordert wird, gibt diese Abschlussfunktion einfach einen Nullzeiger zurück, und die virtuelle QueryInterface-Methode gibt den Fehlercode "E_NOINTERFACE" zurück. Und damit ist also IUnknown versorgt. Wenn Sie sich nur für klassisches COM interessieren, können Sie problemlos hier aufhören, denn das ist alles, was Sie benötigen. Es lohnt sich, an dieser Stelle noch einmal zu wiederholen, dass der Compiler diese QueryInterface-Implementierung mit ihren verschiedenen "rekursiven" Funktionsaufrufen und konstanten Ausdrücken so optimieren wird, dass der Code mindestens so gut ist, wie Sie ihn per Hand schreiben könnten. Und dasselbe können Sie für IInspectable erreichen.

Bei Windows-Runtime-Klassen besteht die zusätzliche Komplexität der Implementierung von IInspectable. Diese Schnittstelle ist bei Weitem nicht so grundlegend wie IUnknown und stellt im Vergleich zu den absolut wesentlichen Funktionen von IUnknown eine eher zweifelhafte Sammlung von Funktionen zur Verfügung. Eine Diskussion dieses Aspekts stelle ich aber für einen zukünftigen Artikel zurück und konzentriere mich hier auf eine effiziente und moderne C++-Implementierung, um jegliche Windows-Runtime-Klasse zu unterstützen. Zuerst muss ich die virtuellen Funktionen "GetRuntimeClassName" und "GetTrustLevel" aus dem Weg räumen. Beide Methoden lassen sich relativ trivial implementieren und werden außerdem selten verwendet, sodass ihre Implementierung durchaus oberflächlich abgehandelt werden kann. Die GetRuntimeClassName-Methode sollte eine Windows-Runtime-Zeichenfolge mit dem vollständigen Namen der Runtime-Klasse zurückgeben, die von dem Objekt dargestellt wird. Ich überlasse es der Klasse selbst, dies zu implementieren, falls sie sich dazu entschließt. Die Implements-Klassenvorlage kann einfach "E_NOTIMPL" zurückgeben, um anzuzeigen, dass diese Methode nicht implementiert ist:

HRESULT __stdcall GetRuntimeClassName(HSTRING * name) noexcept
{
  *name = nullptr;
  return E_NOTIMPL;
}

Ähnlich gibt die GetTrustLevel-Methode einfach eine aufgezählte Konstante zurück:

HRESULT __stdcall GetTrustLevel(TrustLevel * trustLevel) noexcept
{
  *trustLevel = BaseTrust;
  return S_OK;
}

Beachten Sie, dass ich diese IInspectable-Methoden nicht ausdrücklich als virtuelle Funktionen kennzeichne. Die Vermeidung der virtuellen Deklaration erlaubt es dem Compiler, diese Methoden zu strippen, falls die COM-Klasse nicht wirklich IInspectable-Schnittstellen implementiert. Ich wende mich nun der IInspectable-Methode "GetIids" zu. Diese ist sogar noch fehleranfälliger als QueryInterface. Obgleich ihre Implementierung nicht auch nur annähernd so kritisch ist, ist dennoch eine vom Compiler generierte Implementierung wünschenswert. GetIids gibt ein dynamisch zugeordnetes Array aus GUIDs zurück. Jede GUID stellt eine Schnittstelle dar, die das Objekt zu implementieren vorgibt. Sie könnten anfangs annehmen, dass dies einfach eine Deklaration dessen ist, was das Objekt über QueryInterface unterstützt, aber dies trifft nur auf den ersten Blick zu. Die GetIids-Methode könnte sich entschließen, die Veröffentlichung mancher Schnittstellen zurückzuhalten. Jedenfalls beginnen wir mit ihrer grundlegenden Definition:

HRESULT __stdcall GetIids(unsigned long * count, 
  GUID ** array) noexcept
{
  *count = 0;
  *array = nullptr;

Der erste Parameter zeigt auf eine vom Aufrufer bereitgestellte Variable, die die GetIids-Methode im resultierenden Array auf die Anzahl der Schnittstellen festlegen muss. Der zweite Parameter zeigt auf ein Array aus GUIDs, und über ihn gibt die Implementierung das dynamisch zugeordnete Array an den Aufrufer zurück. Hier habe ich nun begonnen, indem ich beide Parameter gelöscht habe – nur zur Sicherheit. Jetzt muss ich bestimmen, wie viele Schnittstellen die Klasse implementiert. Ich würde Ihnen gerne raten, einfach den sizeof-Operator zu verwenden, der die Größe eines Parameterpakets wie folgt bereitstellen kann:

unsigned const size = sizeof ... (Interfaces);

Das ist ziemlich praktisch, und der Compiler kann die Anzahl der Vorlagenargumente melden, die vorhanden wären, wenn dieses Parameterpaket erweitert würde. Dies ist tatsächlich auch ein konstanter Ausdruck, der einen Wert erzeugt, der während der Kompilierung bekannt ist. Der Grund, warum das, wie schon zu Beginn angedeutet, nicht funktionieren wird, ist, dass es bei Implementierungen von GetIids ausgesprochen häufig vorkommt, dass einige Schnittstellen zurückgehalten werden, die mit niemandem gemeinsam genutzt werden sollen. Solche Schnittstellen werden auch als "verdeckte Schnittstellen" bezeichnet. Jeder kann sie mittels QueryInterface abfragen, aber GetIids wird Ihnen nicht verraten, dass sie verfügbar sind. Deshalb muss ich einen Ersatz für den variadic-Operator "sizeof" während der Kompilierung bereitstellen, der verdeckte Schnittstellen ausschließt. Außerdem muss ich eine Methode zur Verfügung stellen, um solche verdeckten Schnittstellen zu deklarieren und zu identifizieren. Ich werde mit Letzterem beginnen. Ich möchte die Implementierung von Klassen so einfach wie möglich halten für Komponentenentwickler, weshalb ein relativ unauffälliger Mechanismus zum Einsatz kommt. Ich kann einfach eine Cloaked-Klassenvorlage bereitstellen, um verdeckte Schnittstellen "auszuzeichnen":

template <typename Interface>
struct Cloaked : Interface {};

Dann kann ich entscheiden, dass eine spezielle "IHenNative"-Schnittstelle zur konkreten Hen-Klasse implementiert wird, die nicht allen Consumern bekannt ist:

class Hen : public Implements<IHen, IHen2, Cloaked<IHenNative>>
{
};

Da die Cloaked-Klassenvorlage von ihrem Vorlagenargument abgeleitet wird, funktioniert die vorhandene QueryInterface-Implementierung auch weiterhin reibungslos. Ich habe lediglich ein paar zusätzliche Typinformationen hinzugefügt, die ich jetzt, auch wieder während der Kompilierung, abfragen kann. Hierfür definiere ich eine "IsCloaked"-Typeigenschaft, damit ich alle Schnittstellen problemlos abfragen kann, um zu bestimmen, ob die jeweilige Schnittstelle verdeckt wurde:

template <typename Interface>
struct IsCloaked : std::false_type {};
template <typename Interface>
struct IsCloaked<Cloaked<Interface>> : std::true_type {};

Jetzt kann ich die Anzahl der nicht verdeckten Schnittstellen erneut mithilfe einer rekursiven variadic-Funktionsvorlage zählen:

template <typename First, typename ... Rest>
constexpr unsigned CounInterfaces() noexcept
{
  return !IsCloaked<First>::value + CounInterfaces<Rest ...>();
}

Und natürlich benötige ich eine Abschlussfunktion, die einfach null zurückgeben kann:

template <int = 0>
constexpr unsigned CounInterfaces() noexcept
{
  return 0;
}

Die Möglichkeit zur Durchführung solch arithmetischer Berechnungen während der Kompilierung mit modernem C++ ist erstaunlich leistungsfähig und verblüffend einfach. Ich kann jetzt damit fortfahren, die GetIids-Implementierung auszuarbeiten, indem ich diese Anzahl anfordere:

unsigned const localCount = CounInterfaces<Interfaces ...>();

Die einzige Einschränkung ist, dass die Unterstützung des Compilers für konstante Ausdrücke noch nicht besonders ausgereift ist. Wenn es sich hier auch zweifellos um einen konstanten Ausdruck handelt, berücksichtigt der Compiler noch keine constexpr-Memberfunktionen. Idealerweise könnte ich die CountInterfaces-Funktionsvorlagen als "constexpr" markieren, sodass der resultierende Ausdruck dann ebenfalls ein konstanter Ausdruck wäre, aber der Compiler sieht das zurzeit noch anders. Andererseits habe ich keinen Zweifel, dass der Compiler dennoch in der Lage sein wird, diesen Code zu optimieren. Wenn nun CounInterfaces, aus welchen Gründen auch immer, keine nicht verdeckten Schnittstellen findet, kann GetIids einfach eine Erfolgsmeldung zurückgeben, weil das resultierende Array leer sein wird:

if (0 == localCount)
{
  return S_OK;
}

Wiederum ist dies tatsächlich ein konstanter Ausdruck, und der Compiler wird den Code ohne Bedingung auf die eine oder andere Art generieren. Mit anderen Worten, wenn keine nicht verdeckten Schnittstellen vorhanden sind, wird der restliche Code einfach aus der Implementierung entfernt. Andernfalls ist die Implementierung gezwungen, ein ausreichend großes Array aus GUIDs mithilfe der traditionellen COM-Zuweisung zuzuordnen:

GUID * localArray = static_cast<GUID *>(CoTaskMemAlloc(sizeof(GUID) * localCount));

Natürlich kann dies fehlschlagen, sodass ich in diesem Fall einfach das geeignete HRESULT zurückgebe:

if (nullptr == localArray)
{
  return E_OUTOFMEMORY;
}

An diesem Punkt verfügt GetIids über ein Array, das bereit ist, mit GUIDs aufgefüllt zu werden. Wie Sie sich denken können, muss ich die Schnittstellen ein letztes Mal aufzählen, um die GUID jeder nicht verdeckten Schnittstelle in dieses Array zu kopieren. Hierzu verwende ich wie zuvor schon ein Paar aus Funktionsvorlagen:

template <int = 0>
void CopyInterfaces(GUID *) noexcept {}
template <typename First, typename ... Rest>
void CopyInterfaces(GUID * ids) noexcept
{
}

Die variadic-Vorlage (die zweite Funktion) kann einfach die "IsCloaked"-Typeigenschaft verwenden, um zu bestimmen, ob die GUID der Schnittstelle, die durch das First-Vorlagenargument identifiziert wurde, vor dem Erhöhen des Zählers kopiert werden soll. Auf diese Weise, wird das Array durchlaufen, ohne dass Sie nachverfolgen müssen, wie viele Elemente es enthält oder an welche Stelle Elemente geschrieben werden sollten. Ich unterdrücke außerdem die Warnung wegen dieses konstanten Ausdrucks:

#pragma warning(push)
#pragma warning(disable:4127) // Conditional expression is constant
if (!IsCloaked<First>::value)
{
  *ids++ = __uuidof(First);
}
#pragma warning(pop)
CopyInterfaces<Rest ...>(ids);

Wie Sie sehen können, verwendet der "rekursive" Aufruf von CopyInterfaces am Ende den potenziell erhöhten Zeigerwert. Und fast bin ich fertig. Die GetIids-Implementierung kann dann mit dem Aufrufen von CopyInterfaces beendet werden, um das Array aufzufüllen, bevor es an den Aufrufer zurückgegeben wird:

CopyInterfaces<Interfaces ...>(localArray);
  *count = localCount;
  *array = localArray;
  return S_OK;
}

Und während des gesamten Vorgangs hat die konkrete Hen-Klasse keine Ahnung von all der Arbeit, die der Compiler in ihrem Dienst erledigt:

class Hen : public Implements<IHen, IHen2, Cloaked<IHenNative>>
{
};

Und genauso sollte es bei jeder guten Bibliothek sein. Der Visual C++ 2015-Compiler bietet eine unglaubliche Unterstützung für Standard-C++ auf der Windows-Plattform. Er ermöglicht C++-Entwicklern, außerordentlich elegante und effiziente Bibliotheken zu erstellen. Dies unterstütz sowohl die Entwicklung von Windows-Runtime-Komponenten in Standard-C++ sowie deren Benutzung in universellen Windows-Apps, die vollständig in Standard-C++ geschrieben sind. Die Implements-Klassenvorlage ist lediglich ein Beispiel aus modernem C++ für die Windows-Runtime (siehe unter 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