C++

Windows API で最新の C++ を使えるようにする Visual C++ 2015

Kenny Kerr

Visual C++ 2015 は、Windows プラットフォームで最新の C++ を使えるようにするために C++ チームが投じてきた多大な努力の集大成です。直近の数回のリリースで、Visual C++ には、最新の C++ 言語とライブラリの豊富な機能が選別され、順次取り入れられてきました。こうした機能を組み合わせて使用すれば、ユニバーサル Windows アプリやコンポーネントをビルドするための非常に優れた環境を生み出すことができます。Visual C++ 2015 は、これまでのこうしたリリースによるすばらしい強化点を軸に、C++11 の大部分と C++ 2015 のサブセットをサポートする成熟度の高いコンパイラを提供します。完成度に疑問を持つ方もいるかもしれませんが、コンパイラは特に重要な言語機能をサポートしており、Windows のライブラリ開発における新時代が、最新の C++ によって幕を開けたといっても過言ではありません。これこそが鍵となります。効率的で洗練されたライブラリの開発がコンパイラでサポートされる限り、開発者は、優れたアプリとコンポーネントのビルドをスムーズに進めることができます。

今回は、新機能を羅列したり、機能の概要を慌ただしく説明するのではなく、これまで複雑だと思われてきたいくつかのコードを作成する方法について、順を追って説明します。実を言うと、Visual C++ コンパイラが成熟してきたことで、こうしたコードを非常に楽しんで作成できるようになりました。ここでは Windows にとって本質的なだけでなく、現在および今後重要な API のほぼすべての心臓部になることについて説明します。

最新の C++ になってようやく COM を十分にサポートできるようになったというのは、やや皮肉なものです。もちろん、COM とは Component Object Model のことです。長年多くの Windows API で基盤となり、引き続き Windows ランタイムでも基盤となる COM です。COM 本来の設計は紛れもなく C++ に結び付いており、バイナリやセマンティック規則については多くが C++ から流用されています。しかし、全体としては洗練さに欠けています。C++ で十分な移植性がない部分 (dynamic_cast など) は、ソリューションの移植性を損なわないよう使用を避ける必要があり、これが C++ 実装を開発しにくくしている原因です。長年にわたり、C++ 開発者は COM をより快適に使用できるように数々のソリューションを考え出してきました。C++/CX 言語拡張機能は、おそらく、Visual C++ チームが考案したこれまでで最も大胆なソリューションです。皮肉なことに C++/CX は、標準 C++ サポートを向上するための取り組みからは置き去りにされ、そのため言語拡張機能が余計な存在となっているのが現実です。

このことを証明するために、IUnknown と IInspectable をすべて最新の C++ で実装する方法を示します。この 2 つのインターフェイスに目新しい点や魅力的な点は特にありません。IUnknown は、DirectX などの主要 API で中心的な抽象化という役割を担い続けています。このインターフェイスは、IUnknown から派生した IInspectable とともに Windows ランタイムの心臓部を担います。今回は、これらのインターフェイスを、言語拡張機能、インターフェイス テーブル、またはその他のマクロを使わずに実装する方法を紹介します。使用するのは、充実した型情報を数多く備えた、効率的で洗練された C++ のみです。この型情報により、開発者はコンパイラとの優れた対話操作を実現し、何をビルドする必要があるかを把握できます。

主な課題は、COM や Windows ランタイム クラスが実装するインターフェイスのリストを表現する方法を考え出すことと、開発者にとって利便性が高く、コンパイラからアクセスできる方法で実装を行うことです。具体的には、型を格納したこのリストを使用できるようにして、コンパイラがインターフェイスを照会し、列挙まで行えるようにする必要があります。これを実現できれば、コンパイラは IUnknown QueryInterface メソッドのコードのほか、必要に応じて IInspectable GetIids メソッドのコードを生成できる可能性があります。最大の課題は、これら 2 つのメソッドから生じます。以前から、唯一存在するソリューションには言語拡張、不愉快なマクロ、または管理しにくい多数のコードが含まれていました。

2 つのうちどちらのメソッドを実装するにも、クラスが実装する予定のインターフェイスのリストが必要です。通常、このような型のリストを表現するには、可変個引数テンプレートを使用します。

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

novtable __declspec 拡張属性により、コンストラクターとデストラクターでこうした抽象クラスの vfptr を初期化する必要がなくなります。その結果、多くの場合コードのサイズが大幅に縮小されます。Implements クラス テンプレートには、テンプレート パラメーター パックが含まれているため、可変個引数テンプレートになっています。パラメーター パックは、任意の数のテンプレート引数を受け取ることができるテンプレート パラメーターです。ポイントは、通常、関数が任意の数の引数を受け取れるように使用するパラメーター パックを、この例ではコンパイル時にだけ引数が照会されるテンプレートを表すために使用している点です。インターフェイスが関数パラメーターのリストに現れることはありません。

こうした引数の使用方法の 1 つは既に明らかです。パラメーター パックは、基本パブリック クラスをリストの形式で展開します。もちろん、これらの仮想関数を実際に実装しなければならないことには変わりありませんが、この時点で、任意の数のインターフェイスを実装できる具象クラスを作成できます。

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

パラメーター パックは、基本クラスのリストを指定するために展開されるため、上記のコードは手作業で記述する次のようなコードに相当します。

class Hen : public IHen, public IHen2
{
};

このような方法で Implements クラス テンプレートを構造化するメリットは、Implements クラス テンプレートにさまざまな定型コード実装を挿入しても、Hen クラスの開発者はこの目立たない抽象化を使用して、水面下で行われる処理の大半を無視できるようになることです。

ここまでは問題ありません。次に、IUnknown 自体の実装について考えます。現時点でコンパイラが自由に処理できる情報の種類を考えると、IUnknown は完全に Implements クラス テンプレート内部に実装できるはずです。IUnknown には、COM クラスにとって不可欠な 2 つの機能があります。人間にとっての酸素や水のようなものです。1 つ目は参照カウントです。これは、おそらく 2 つの機能でもシンプルな方で、COM オブジェクトが自身の有効期間の追跡に使用する手段です。COM では、一種の煩わしい参照カウントを使用するように定められています。参照カウントにより、各オブジェクトは、未処理の参照の数を把握してオブジェクトの有効期間を管理します。これは、共有する所有権をオブジェクトが意識しない、C++11 shared_ptr クラス テンプレートなどの参照カウント スマート ポインターとは対照的です。2 つのアプローチのどちらを使用するかは意見が分かれるかもしれませんが、現実には COM のアプローチの方が効率性が高いことが多く、また、これが COM の通常の動作であるため、これに対応する必要があります。少なくとも、COM インターフェイスを shared_ptr 内部にラップすることが恐ろしい考えであることには賛同していただけるでしょう。

まず、Implements クラス テンプレートによって唯一発生するランタイム オーバーヘッドについて説明します。

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

既定に設定したコンストラクター自体はオーバーヘッドではありません。コンストラクターを既定のコンストラクターにすることで、これを public ではなく protected に設定しているだけです。既定のコンストラクターでは参照カウントを初期化します。参照カウントと仮想デストラクターは、どちらも protected にします。派生クラスから参照カウントにアクセスできるようにすると、より複雑なクラス構成が可能になります。ほとんどのクラスではこれを単純に無視できますが、上記のコードでは参照カウントを 1 に初期化しています。これは、参照がまだ渡されていないことから、参照カウントの初期値を 0 に設定することを推奨する一般的な教え方に反しています。このアプローチは、ATL によって一般的に広められ、明らかに Don Box の『Essential COM』による影響を受けたものです。しかし、ATL ソース コードに関する調査で十分に証明されたように、このアプローチにはかなり問題があります。参照を所有していることが、すぐに呼び出し元によって推測される、またはスマート ポインターに渡されると初めから想定しておくと、作成プロセスのエラーははるかに発生しにくくなります。

仮想デストラクターは、具象クラス自体に実装を強制的に用意させるのではなく、Implements クラス テンプレートが参照カウントを実装できるため、非常に便利です。もう 1 つのオプションは、仮想関数を避けるために、Curiously Recurring Template Pattern (奇妙に再帰したテンプレート パターン) を使用することです。通常、個人的にはこのようなアプローチを使用しますが、抽象化が少し複雑になります。また、COM クラスには最初から vtable が用意されているため、仮想関数を使用しない正当な理由はありません。これらのプリミティブがあれば、AddRef と Release の両方を Implements クラス テンプレートに簡単に実装できます。まず、AddRef メソッドでは、組み込みの InterlockedIncrement を使用して参照カウントを増やします。

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

大部分は見てのとおりです。条件に応じて、組み込み関数の InterlockedIncrement と InterlockedDecrement を、C++ のインクリメント演算子やデクリメント演算子に置き換えるような複雑な手法を考え出そうとはしないでください。この処理を試みる ATL に、大きな複雑さが生まれるためです。効率を重視する場合は、AddRef と Release の見せかけの呼び出しを避けるよう努めます。この場合も移動のセマンティクスをサポートし、参照を増やさずに参照の所有権を移動できる最新の C++ の出番です。Release メソッドの複雑さは、AddRef とそれほど変わりません。

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

参照カウントをデクリメントし、結果をローカル変数に代入します。この結果を返す必要があるため、この処理は重要です。しかしオブジェクトを破棄した場合は、メンバー変数を参照するのは不適切です。ここでは未処理の参照はないと仮定し、前述の仮想デストラクターを呼び出して単純にオブジェクトを削除しています。これで参照カウントは終了です。Hen 具象クラスの簡潔さはこれまでと変わりありません。

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

今度は、QueryInterface というすばらしい世界について考えます。この IUnknown メソッドを実装することは容易ではありません。これについては、Pluralsight コースで詳しく取り上げています。また、Don Box が執筆した『Essential COM』(Addison-Wesley Professional、1998 年) では、独自の実装を始めるための奇妙かつすばらしい方法を数多く確認できます。この書籍は COM に関する優れた教科書です。ただし、C++98 に基づいていて、決して最新の C++ について説明しているものではないことに注意してください。スペースと時間を節約するため、ここでは QueryInterface の実装についてある程度の知識があると仮定し、最新の C++ を使用する QueryInterface の実装方法に注目します。以下に、仮想メソッドを示します。

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

特定のインターフェイスを識別する GUID を前提として、QueryInterface は、オブジェクトが目的のインターフェイスを実装するかどうかを判断します。実装する場合は、そのオブジェクトの参照カウントをインクリメントし、out パラメーターを使って目的のインターフェイス ポインターを返す必要があります。実装しない場合は、null ポインターを返す必要があります。そこで、まずは概略を示します。

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

QueryInterface は、最初になんらかの方法で目的のインターフェイスを見つけようとします。そのインターフェイスがサポートされていない場合、必要な E_NOINTERFACE エラー コードを返します。上記のコードではこの要件を既に満たし、作成されたインターフェイス ポインターに問題がある場合は、そのインターフェイス ポインターをクリアしています。QueryInterface は、バイナリ演算とほぼ同じものと考えてください。結果は、目的のインターフェイスを見つけられたか見つけられなかったかのいずれかです。創造力を働かせ、条件を付けて都合よく応答する必要はありません。COM 仕様で許可されている限定的なオプションもいくつかありますが、開発者がどのようなエラー コードを返そうと、ほとんどのコンシューマーはインターフェイスがサポートされていないとしか考えません。実装に少しでもミスがあれば、間違いなく終わりのないデバッグ地獄に陥るでしょう。QueryInterface は、あれこれと手を加えるにはあまりにも基本的なメソッドです。最後に、作成されたインターフェイス ポインターを再び使用して AddRef を呼び出し、頻度は少なくても可能性がある、いくつかのクラス構成シナリオをサポートしています。この処理は Implements クラス テンプレートで明示的にサポートされているわけではありませんが、ここではむしろ優れた例として紹介しています。参照カウント操作は、オブジェクト固有ではなくインターフェイス固有であることを意識します。1 つのオブジェクトに属するすべてのインターフェイスで AddRef や Release を呼び出せるわけではありません。オブジェクト ID を管理する COM 規則に従う必要があります。COM 規則に従わない場合、不可解な方法で中断する不適切なコードになるおそれがあります。

では、要求された GUID が、クラスが実装するインターフェイスを表すかどうか判断するにはどうすればよいでしょう。ここで、Implements クラス テンプレートがテンプレート パラメーター パックによって収集する型情報に話を戻します。目標は、コンパイラがこの機能を自動的に実装できるようにすることです。作成するコードでは、手動で記述したコードと同程度、またはそれ以上の効率を実現することを考えます。したがって、このクエリの実行には、一連の可変個引数関数テンプレート (テンプレート自体にテンプレート パラメーター パックが備わっている関数テンプレート) を使用します。まず、BaseQueryInterface 関数テンプレートから取り掛かります。

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

BaseQueryInterface は、基本的には IUnknown QueryInterface の最新の C++ プロジェクションです。HRESULT を返す代わりにインターフェイス ポインターを直接返します。エラーは、null ポインターによって明確に示します。また、1 つの関数引数 (検索するインターフェイスを識別する GUID) を受け取ります。さらに重要なのは、BaseQueryInterface 関数がインターフェイスの列挙操作を開始できるように、クラス テンプレートのパラメーター パックを全体的に展開している点です。Implements クラス テンプレートのメンバーである BaseQueryInterface は、このインターフェイスのリストに単純に直接アクセスできると思われるかもしれません。しかし、次に示すように、この関数ではリストの 1 つ目のインターフェイスを切り離せるようにする必要があります。

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

このようにすることで、BaseQueryInterface は 1 つ目のインターフェイスを特定し、その他のインターフェイスを以降の検索のために残しておくことができます。おわかりのように、COM にはオブジェクト ID をサポートする際の具体的な規則が数多くあります。QueryInterface はこれらの規則を実装するか、少なくとも規則に従うことが必要です。具体的には、2 つのインターフェイス ポインターが同じオブジェクトを参照しているかどうかをクライアントが判別できるように、IUnknown の要求では常にまったく同じポインターを返す必要があります。そのため、BaseQueryInterface 関数は、こうした原則の一部を実装するのに最適な場所です。要求された GUID を、1 つ目のテンプレート引数 (クラスが実装するインターフェイスの 1 つ目) と比較することから始めます。GUID とテンプレート引数が一致しない場合は、IUnknown が要求されているかどうかを確認します。

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

GUID とテンプレート引数のいずれかが一致したと仮定して、1 つ目のインターフェイスの一意のインターフェイス ポインターを返します。static_cast により、コンパイラが、IUnknown に基づく複数のインターフェイスから生じるあいまいさに対処することは保証されません。このキャストは、クラスの vtable の正しい場所を見つけられるようにポインターを調整するだけです。また、すべてのインターフェイスの vtable は、IUnknown の 3 つのメソッドから始まるため、この処理は非常に有効です。

ここで、IInspectable クエリに対するオプションのサポートを追加してもよいでしょう。IInspectable は、やや奇妙なインターフェイスです。C# や JavaScript のような言語で使用できるすべての Windows ランタイム インターフェイスは、単純に IUnknown から派生するのではなく、直接 IInspectable から派生する必要があるため、IInspectable はある意味 Windows ランタイムの IUnknown とも言えます。共通言語ランタイムによるオブジェクトとインターフェイスの実装方法に対応する場合、これが問題になります。この実装方法が、C++ の動作方式や従来の COM の定義とは反するためです。また、オブジェクトを構成する際、パフォーマンスに少し悪影響を及ぼす問題も発生します (ただし、これについては広範なトピックになるため、今後のコラムで取り上げます)。QueryInterface に関して言えば、必要な作業は、これが単なる従来の COM クラスではなく Windows ランタイム クラスの実装である場合に、IInspectable をクエリできるようにすることだけです。IUnknown に関する明示的な COM 規則は IInspectable には適用されませんが、この例では IUnknown とほぼ同様に IInspectable を扱うことができます。ただし、これには 2 つの課題が伴います。1 つは、実装したインターフェイスの中に、IInspectable から派生したインターフェイスがあるかどうかを確認する必要があることです。2 つ目は、そうしたインターフェイスのいずれかの型が必要であることです。この型により、あいまいさを生むことなく適切に調整されたインターフェイス ポインターを返すことができます。リストの 1 つ目のインターフェイスが常に IInspectable に基づいていると想定できれば、BaseQueryInterface を次のように更新するだけで済む場合もあります。

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

C++11 の型の特徴 is_base_of を使用して、1 つ目のテンプレート引数が IInspectable から派生したインターフェイスかどうかを判別します。このようにすると、Windows ランタイムのサポートを使用しないで従来の COM クラスを実装している場合、これ以降の比較操作がコンパイラによって除外されます。この方法では、コンポーネント開発者が使用する構文がさらに複雑になることも、ランタイム オーバーヘッドが不必要に発生することもなく、Windows ランタイム クラスと従来の COM クラスの両方をシームレスにサポートできます。しかしこれでは、リストの最初に IInspectable から派生していないインターフェイスをある場合、非常に小さなバグが発生する可能性が残ります。is_base_of を、インターフェイスのリスト全体をスキャンできるものに置き換えることが必要です。

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

IsInspectable は依然として型の特徴 is_base_of を使用していますが、今度は一致が見つかるまで is_base_of を各インターフェイスに適用します。IInspectable に基づくインターフェイスが見つからない場合は、終了関数に行き着きます。

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

奇妙な既定の無名引数については、後ほど説明します。IsInspectable が true を返すとすると、IInspectable に基づく 1 つ目のインターフェイスを見つける必要があります。

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

型の特徴 is_base_of を再び使用することもできますが、今回は、一致が見つかった場合に実際のインターフェイス ポインターを返します。

#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 では、FindInspectable と IsInspectable を一緒に使用するだけで、IInspectable のクエリをサポートできます。

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

ここでも、Hen 具象クラスを使用すると、コードは次のようになります。

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

この Implements クラス テンプレートにより、IHen または IHen2 が IInspectable から派生していても、単純に IUnknown (またはその他のインターフェイス) から派生していても、コンパイラは最も効率の高いコードを生成できます。これで、ようやく QueryInterface の再帰部分を実装して、先の例の IHen2 のような追加インターフェイスに対応することができます。BaseQueryInterface では、最後に FindInterface 関数テンプレートを呼び出します。

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);
}

上記の FindInterface 関数テンプレートでは、最初に BaseQueryInterface を呼び出したときとほぼ同じ方法で呼び出しているのがわかります。このコードでは、残りのインターフェイスを FindInterface に渡しています。具体的には、リストに残っているインターフェイスの 1 つ目を FindInterface が再び特定できるように、パラメーター パックを展開しています。しかし、この処理にはある問題があります。それは、テンプレート パラメーター パックは関数引数としては展開できないため、言語を使っても本当に必要なものを表現できないという厄介な状況に陥る可能性があることです。これについては後ほど詳しく説明します。ご想像のとおり、この "再帰的な" FindInterface 可変個引数テンプレートは次のようになります。

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

1 つ目のテンプレート引数を他の引数から分離し、一致が見つかった場合は調整済みのインターフェイス ポインターを返します。そうしないと、このテンプレートは、インターフェイス リストの最後にたどり着くまで自身を呼び出し続けます。ここではこの処理を漠然と "コンパイル時再帰" と呼びます。しかし、この関数テンプレート (および Implements クラス テンプレートに含まれる同じような他の例) は、厳密には "再帰的" でも、"コンパイル時" でもありません。関数テンプレートのインスタンス作成操作では、毎回異なるインスタンス作成操作を呼び出しています。たとえば、FindInterface<IHen, IHen2> は FindInterface<IHen2> を呼び出し、FindInterface<IHen2> は FindInterface<> を呼び出します。この関数テンプレートを再帰的にするには、FindInterface<IHen, IHen2> で FindInterface<IHen, IHen2> を呼び出す必要がありますが、実際の操作とは異なります。

とはいえ、この "再帰" がコンパイル時に発生し、これらの if ステートメントをすべて手動で記述した場合と同じように順序よく動作することに留意してください。ですが、ここで思わぬ問題が発生します。このシーケンスはどのように終了するのでしょう。それはもちろん、テンプレート引数のリストが空になったときです。ここでの問題は、空のテンプレート パラメーター リストが C++ で既に定義されていることです。

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

これでもほぼ適切ですが、コンパイラでは、この特殊化には関数テンプレートを使用できないことが示されます。しかし、この終了関数を使用しなければ、パラメーター パックが空になったときにコンパイラで最後の呼び出しをコンパイルすることができません。関数をオーバーロードしている場合は引数のリストが変更されないため、このような状況は起こりません。さいわい、この問題は実に簡単に解決できます。終了関数に既定の無名引数を使用すれば、終了関数は特殊化として認識されなくなります。

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

これで、コンパイラでの問題はなくなりました。サポートされていないインターフェイスが要求された場合は、この終了関数が null ポインターを返し、QueryInterface 仮想メソッドが E_NOINTERFACE エラー コードを返します。また、この処理では IUnknown にも対応できます。従来の COM のみに対応する場合は、これですべての処理が用意できたため、ここで作業を完了しても問題ありません。重要なことなのでここで繰り返しておきますが、コンパイラはさまざまな "再帰" 関数呼び出しと定数式を使用して、この QueryInterface 実装を最適化します。これにより、少なくとも手動で記述した場合と同品質のコードを作成できます。また、IInspectable でも同様の処理を実現できます。

Window ランタイム クラスの場合は、IInspectable の実装に複雑さが加わります。IInspectable インターフェイスは、非常に基礎的な IUnknown とはかけ離れており、IUnknown の関数が絶対的に必要であるのに対し、提供される機能のコレクションは必要性が不明瞭です。とはいえ、このインターフェイスについても今後に役立つよう説明しておきます。ここでは、あらゆる Windows ランタイム クラスをサポートするための、効率的な最新の C++ 実装に重点を置きます。まず、仮想関数である GetRuntimeClassName と GetTrustLevel については除外します。どちらのメソッドも比較的実装しやすく、使用頻度も非常に低いことから、実装について考慮しなくても大きな問題はありません。GetRuntimeClassName メソッドは、オブジェクトが示すランタイム クラスの完全な名前を含む Windows ランタイム文字列を返します。クラスによってこのメソッドを実装することになった場合、実装についてはそのクラス自体に任せます。Implements クラス テンプレートでは、E_NOTIMPL を返すだけで、このメソッドが実装されていないことを示すことができます。

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

同様に、GetTrustLevel メソッドは列挙型定数を返します。

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

これらの IInspectable メソッドは、仮想関数として明示的にマークしていません。仮想であることを宣言しないことで、COM クラスが IInspectable インターフェイスを実際には実装しない場合、コンパイラはこれらのメソッドを除外できます。ここからは、IInspectable GetIids メソッドに話を移します。GetIids は、QueryInterface よりもさらにエラーが発生しやすいメソッドです。実装する重要性は QueryInterface よりもはるかに低くなりますが、コンパイラが生成する実装では効率がよいことが望まれます。GetIids は、動的に割り当てられた GUID の配列を返します。各 GUID は、オブジェクトが実装する 1 つのインターフェイスを表します。一見、オブジェクトが QueryInterface によってサポートしている内容を宣言しているだけのようにも思えますが、その考えは表向きにしか正解ではありません。GetIids メソッドでは、一部のインターフェイスを公開しないように決めることもできます。いずれにせよ、まずはこのメソッドの基本定義を説明します。

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

1 つ目のパラメーターは、呼び出し元によって提供された変数を指します。GetIids メソッドでは、作成される配列内のインターフェイス数をこの変数に設定する必要があります。2 つ目のパラメーターは、GUID の配列を指します。このパラメーターは、動的に割り当てられる配列を実装で呼び出し元に返す経路になります。ここでは、念のために最初に両方のパラメーターをクリアします。次に、クラスが実装するインターフェイスの数を判断する必要があります。以下に示すように、パラメーター パックのサイズを返す sizeof 演算子を使用することをお勧めします。

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

これは非常に便利な処理です。コンパイラでは、このパラメーター パックを展開した場合に提供されるテンプレート引数の数がレポートされます。また、コンパイル時に認識されている値を生成する実質的な定数式でもあります。先ほど少し触れましたが、この処理が不適切な理由は、無制限に共有することが好ましくない一部のインターフェイスが、GetIids の実装で保留されることが非常に多いからです。このようなインターフェイスは、クローク インターフェイスと呼ばれます。QueryInterface を使ってクローク インターフェイスをクエリすることはできますが、クローク インターフェイスが使用可能かどうかを GetIids から判断することはできません。したがって、クローク インターフェイスを除いた可変個引数の sizeof 演算子を対象とするコンパイル時置換と、このようなクローク インターフェイスを宣言および識別するなんらかの方法を用意する必要があります。まずは、後者について説明します。比較的シンプルなメカニズムを実現するために、コンポーネント開発者ができるだけ簡単にクラスを実装できるようにします。Cloaked クラス テンプレートを使用すると、あらゆるクローク インターフェイスを "修飾" できます。

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

これで、一部のコンシューマーにしか公開されない Hen 具象クラスに、特殊な "IHenNative" インターフェイスを実装できます。

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

Cloaked クラス テンプレートは、自身のテンプレート引数から派生するため、既存の QueryInterface 実装はシームレスに動作し続けます。ここで、またしてもコンパイル時にクエリできるようになる型情報を少し付け加えています。そのため、型の特徴 IsCloaked を定義します。これにより、すべてのインターフェイスを簡単にクエリして、インターフェイスがクロークされているかどうかを判別できます。

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

これで、再帰的な可変個引数関数テンプレートを使用して、クロークされていないインターフェイスの数を再びカウントできるようになります。

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

もちろん、単純に 0 を返す終了関数も必要です。

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

コンパイル時に最新の C++ を使用してこうした計算を実行する機能は、驚くほど強力かつ単純です。引き続き、このカウントを要求して GetIids 実装を具体化します。

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

1 つの欠点は、コンパイラによる定数式のサポートがまだあまり成熟していないことです。上記は紛れもなく定数式です。しかし、コンパイラでは constexpr メンバー関数がサポートされていません。CountInterfaces 関数テンプレートを constexpr としてマークでき、定数式と同様の式が作成されるのが理想ですが、コンパイラはこうした操作にまだ対応できていません。一方、このコードの最適化については、コンパイラでどのように実行しても問題がないことは確かです。なんらかの理由により CounInterfaces でクロークされていないインターフェイスが見つからない場合、作成する配列は空になるため、GetIids では成功を返すだけでかまいません。

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

上記のコードも実質的には定数式です。コンパイラは条件を使った方法を使用せずにコードを生成します。つまり、クロークされていないインターフェイスがなければ、残りのコードは実装から削除されます。それ以外の場合、実装では、従来の COM アロケーターを使用して、適切なサイズの GUID 配列を割り当てるほかありません。

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

言うまでもなく、上記のコードは失敗する可能性があります。その場合は、適切な HRESULT を返します。

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

この時点で、GetIids には GUID を格納できる配列が用意されています。ご想像のとおり、クロークされていない各インターフェイスの GUID をこの配列にコピーするには、インターフェイスをもう一度列挙する必要があります。これには、既に説明したように 2 つの関数テンプレートを使用します。

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

可変個引数テンプレート (2 つ目の関数) では、型の特徴 IsCloaked を使用するだけで、First テンプレート引数で識別したインターフェイスの GUID をコピーするかどうかを、ポインターをインクリメントする前に判断できます。このようにすると、配列に格納される要素の数や配列内で要素が書き込まれる場所を追跡しなくても、配列全体を処理できます。また、この定数式に関する警告も表示しないようにします。

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

ご覧のように、最後の CopyInterfaces の "再帰" 呼び出しでは、推定されるインクリメント後のポインター値を使用しています。これで、作業はほぼ完了です。GetIids 実装の最後に CopyInterfaces を呼び出して、配列に値を格納した後、配列を呼び出し元に返します。

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

その間、Hen 具象クラスは、コンパイラが Hen 具象クラスに代わって実行している処理について一切認識しません。

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

これは、優れたライブラリに望ましいことです。Visual C++ 2015 コンパイラでは、Windows プラットフォームに関する標準 C++ の優れたサポートが提供されます。C++ 開発者は、非常に洗練された、効率的なライブラリを実現できます。これにより、標準 C++ での Windows ランタイム コンポーネント開発と、そのコンポーネントをすべて標準 C++ で記述されたユニバーサル Windows アプリで使用する機能が両方ともサポートされます。Implements クラス テンプレートは、最新の C++ で Windows ランタイムに使用できる 1 つの例にすぎません (moderncpp.com (英語) を参照してください)。


Kenny Kerr は、カナダを拠点とするコンピューター プログラマであり、Pluralsight の執筆者です。また、Microsoft MVP でもあります。彼のブログは kennykerr.ca (英語) で、Twitter は twitter.com/kennykerr (英語) でフォローできます。

この記事のレビューに協力してくれたマイクロソフト技術スタッフの James McNellis に心より感謝いたします。