前書き
C++/CLI には型パラメータ変数を扱うのに generics と template の2種類が用意されています。
この二つ、構文が似ているせいか一緒くたにされて、古くからの C++ ユーザーには generics は駄目だとか何とか、あらぬ誤解を受けていたりしていますが、ちゃんと使い分けの境界がはっきりしています。
ここでは、generics と template の境界を説明し、どんなときにどちらを使えばいいのか、わかりやすく整理したいと思います。
それぞれの特徴
template はぶっちゃげ、高機能なマクロです。
その本質的な処理は「文字置き換え」による型変形であり、実装への適合です。
そのため、template には以前から次のような問題が存在します。
- ライブラリ化できない。
template ではヘッダに実装を記述しています。これは template がコンパイル時に展開されるということから来ています。
そのため、template を含むライブラリを配布する場合、ヘッダを付加して配布しなければなりません。
一応、テンプレートを export できる実装系も存在していますが、実装の困難さから少なくとも VC8 では実装されていません。
C++/CLI は VES と呼ばれる仮想エンジンを前提として作成されています。
仮想エンジンは基本的にアセンブリと呼ばれる物理単位(まぁ、DLL)をロードし、エントリ・ポイント(要は、main 関数)を持つアセンブリをアプリケーションとして実行します。
そのため、ライブラリに相当するアセンブリ中には、外部に公開する事が可能な型と内部的にのみ使用できる型に分類されており、型の情報をアセンブリ自身が管理しています。
これまで、C や C++ では、ライブラリにヘッダが付いているのは当たり前のことでした。型情報はそこにしかありませんし、リンクするためのシンボル情報も必要だったからです。ですが、.net ではアセンブリ自体が型情報を保持しているため、アセンブリのみの配布が行われることとなります。
.net の環境ではライブラリの配布とはアセンブリの配布に当たります。アセンブリには公開可能なクラスや関数の情報が記述されており、その安全性をベンダーが保証するためにベンダーによって署名されています。
generics ではコンパイル時には生成型(constructed type)という型を生成し、アセンブリに登録します。
この生成型はまだ型が確定しておらず、アダプトする型が与えられるのを待機している状況にあります。
もちろん、アセンブリ外に非公開な generics (クラスまたは関数)の場合には、すでに使用されることで使われる型が確定しているので、これらの生成型は「閉じて」います。そうでない、型を引数として確定するのを待機しているものを「開いた」生成型と呼びます。
TestClazz.cpp
#using <mscorlib.dll>
using namespace System;
template <typename T>
public ref class TestTempClazz
{
public:
TestTempClazz()
{
Console::WriteLine("TestTempClazz constructor.");
}
virtual ~TestTempClazz()
{
Console::WriteLine("TestTempClazz destructor.");
}
!TestTempClazz()
{
Console::WriteLine("TestTempClazz finalizer.");
}
private:
T _param;
};
generic <typename T>
public ref class TestGenClazz
{
public:
TestGenClazz()
{
Console::WriteLine("TestGenClazz constructor.");
}
virtual ~TestGenClazz()
{
Console::WriteLine("TestGenClazz destructor.");
}
!TestGenClazz()
{
Console::WriteLine("TestGenClazz finalizer.");
}
private:
T _param;
};
このファイルからアセンブリを作ってみます。
cl /clr /ld TestClazz.cpp
とすることで、TestClazz.dll アセンブリが作成できます。
このとき、それぞれの型をインスタンス化してみましょう。
sample.cpp
#using <mscorlib.dll>
#using "TestClazz.dll"
using namespace System;
int main()
{
TestGenClazz<String^>^ test = gcnew TestGenClazz<String^>;
TestTempClazz<String^>^ temp = gcnew TestTempClazz<String^>;
return 0;
}
これをコンパイルすると、
cl /clr sample.cpp 次のようなエラーが発生します。
C:\temp>cl /clr sample.cpp
Microsoft(R) C/C++ Optimizing Compiler Version 14.00.50215.44
for Microsoft(R) .NET Framework Version 2.00.50215.44
Copyright (C) Microsoft Corporation. All rights reserved.
sample.cpp
sample.cpp(10) : error C2065: 'TestTempClazz' : 定義されていない識別子です。
sample.cpp(10) : error C2275: 'System::String' : この型は演算子として使用できません
c:\windows\microsoft.net\framework\v2.0.50215\mscorlib.dll : 'System::String'
の宣言を確認してください。
sample.cpp(10) : error C2059: 構文エラー : '>'
これはつまり、TestTempClazz<String^> なる型が存在しないことを説明しています。
「テンプレートはアセンブリを越えて型を提供できない」ということがおわかりになるでしょうか?
では、どんなときに template を利用すればいいのでしょうか?
それは「アセンブリ独立を守ることができる範囲」、と言うことになります。
上述した TestTempClazz もそのアセンブリ内での使用に限定するなら、無事にコンパイルし自由に使うことが可能です。その時、残念ながら class に対する修飾子(上層型識別子:
Top level visibility )は private になります。
この上層型識別子はアセンブリ中に定義したクラスを公開するかどうかを指定するものです。
まとめ
上記のように、generics と template はそれぞれに適用範囲を持っています。
- アセンブリ外に公開するクラスや関数は generics で実装
- アセンブリ内で閉じている記述やアセンブリを利用しないのなら template を使ってもいい
この点を意識すると、どこが template でいいのか、どこが generics でなければならないのか、がはっきりすると思われます。
Topに戻る