- 2009年6月 1日 13:35
- .NET Framework | C++/CLI
C++では、メンバ関数テンプレートを仮想関数にできない。ところが、C++/CLI ではテンプレートに良く似た機能である generic を仮想関数につけることができる。これにはどんなトリックが隠されているのだろう?
まずは、次のようなコードから。
public ref class Base // ref class なので参照型
{
public:
generic <typename T> // generic を仮想関数につける
virtual int Func(T in) { // T は任意の型
return sizeof(in); // T 型のサイズを返す(本当は size_t だけど)
}
};
public ref class Derived: public Base
{
public:
generic <typename T>
virtual int Func(T in) override { // Base::Func を override
return sizeof(in) * 2; // T 型のサイズを倍にして返す
}
};
これはTがどんな型であっても正しく動作する。Derived のインスタンスを Base のハンドルで参照しても、Derived 側の Func が呼ばれる。完璧だ。
C++ では、Tの型が決まらないと仮想関数テーブルを構築できず、Tの型を知るためには全ての翻訳単位を調べなければならない。よって、このようなメンバ関数は禁止されている。そして、以前のエントリでは「なんだ、その都度 JIT コンパイルすればいいじゃん」という結論で終わっていた。今回は、これをもう少し調べてみようと思う。
「JIT コンパイルすればいいじゃん」と言ってはみたものの、JIT コンパイラはソースコードを直接ネイティブコードにコンパイルするわけではない。ソースコードは MSIL という中間言語にコンパイルされており、JIT コンパイラは、この MSIL をネイティブコードにコンパイルするだけだ。
ということは、C++ の「仮想関数テーブル作成問題」は、この MSIL を作成する段階で問題にならないだろうか。先ほどのコードを ILDasm で見てみると、このようなコードになっていた。
.method public hidebysig newslot virtual
instance int32 Func<T>(!!T 'in') cil managed
{
// Code size 9 (0x9)
.maxstack 1
.locals ([0] int32 V_0)
IL_0000: sizeof !!T
IL_0006: stloc.0
IL_0007: ldloc.0
IL_0008: ret
} // end of method Base::Func
.method public hidebysig virtual instance int32
Func<T>(!!T 'in') cil managed
{
// Code size 11 (0xb)
.maxstack 2
.locals ([0] int32 V_0)
IL_0000: sizeof !!T
IL_0006: ldc.i4.2
IL_0007: mul
IL_0008: stloc.0
IL_0009: ldloc.0
IL_000a: ret
} // end of method Derived::Func
よく見ると、なんと T が T のままコンパイルされている。そして、sizeof が T をそのまま処理している。MSIL のレベルで T を T として扱っているということは、JIT コンパイルの段階で T の型に応じたネイティブコードを生成可能ということ。なるほど、ここまでは予想通り。
でも、これは MSIL の sizeof が T を T として扱えることに頼っている。sizeof じゃなかったらどうなるのだろう。
public ref class Base
{
public:
generic <typename T>
virtual System::String^ Func(T in) {
return in->ToString(); // 今度は T に対して ToString メソッドを呼び出してみた
}
};
public ref class Derived: public Base
{
public:
generic <typename T>
virtual System::String^ Func(T in) override {
return in->ToString() + " (Derived)"; // Derived 側の処理も上書き
}
};
今度は T 型の変数 in に対して ToString メソッドを呼び出してみた。ILDasm の出力は次の通り。
.method public hidebysig newslot virtual
instance string Func<T>(!!T 'in') cil managed
{
// Code size 16 (0x10)
.maxstack 1
.locals ([0] string V_0)
IL_0000: ldarga.s 'in'
IL_0002: constrained. !!T
IL_0008: callvirt instance string [mscorlib]System.Object::ToString()
IL_000d: stloc.0
IL_000e: ldloc.0
IL_000f: ret
} // end of method Base::Func
.method public hidebysig virtual instance string
Func<T>(!!T 'in') cil managed
{
// Code size 26 (0x1a)
.maxstack 2
.locals ([0] string V_0)
IL_0000: ldarga.s 'in'
IL_0002: constrained. !!T
IL_0008: callvirt instance string [mscorlib]System.Object::ToString()
IL_000d: ldstr " (Derived)"
IL_0012: call string [mscorlib]System.String::Concat(string,
string)
IL_0017: stloc.0
IL_0018: ldloc.0
IL_0019: ret
} // end of method Derived::Func
IL_0008 の部分で、callvirt という命令を使って Object::ToString() を呼び出している。この部分に T はなく、T は1つ前の constrained で使われている。T がどんな型であっても constrained が良きに計らってくれて、実行時の型に応じた ToString() が呼ばれるというわけだ。すばらしい。
ToString は System::Object のメソッドなので、どんな型からも(場合によってはボックス化を伴って)呼び出すことができる。もう少しハードルを上げてみよう。T という型が持っている「かも知れない」メソッドを呼び出してみたらどうだろう。
public ref class Base
{
public:
generic <typename T>
virtual System::String^ Func(T in) {
return in->Func1(); // エラー: T は System::Object と見なされるため、Func1 は呼べない
}
};
public ref class Derived: public Base
{
public:
generic <typename T>
virtual System::String^ Func(T in) override {
return in->Func2(); // エラー: Func2 も System::Object のメンバではない
}
};
エラーだ。コンパイルできない。どうやら System::Object のメンバではないメソッドを呼び出すことができないようだ。このエラーを取り除くためには、次のように T が何者なのかを教えてやる必要がある。
public interface class HasFunc {
System::String^ Func1();
System::String^ Func2();
};
public ref class Base
{
public:
generic <typename T> where T: HasFunc // T が何者かを教える
virtual System::String^ Func(T in) {
return in->Func1(); // T は HasFunc なので、Func1 を呼べる
}
};
public ref class Derived: public Base
{
public:
generic <typename T> // override しているので、T が HasFunc であることは既知
virtual System::String^ Func(T in) override {
return in->Func2(); // T は HasFunc なので、Func2 を呼べる
}
};
ふむふむ、なるほど。先ほどは T が何者なのか分からなかったので、どんな型からも呼び出すことができる System::Object のメソッドしか受け付けなかったというわけか。今回は、T が HasFunc というインタフェースを持っていることが分かっているので、Func1 も Func2 を呼び出せる......って、当たり前じゃないか。
それなら、こんな風にしても同じ事。インタフェースを知る必要があるなら、あえて generic にしなくても書ける。
public interface class HasFunc {
System::String^ Func1();
System::String^ Func2();
};
public ref class Base
{
public:
virtual System::String^ Func(HasFunc^ in) { // 無理に generic にしなくても、こうすればよいだけ
return in->Func1(); // in は HasFunc なので、Func1 を呼び出せる
}
};
public ref class Derived: public Base
{
public:
virtual System::String^ Func(HasFunc^ in) override {
return in->Func2(); // Func2 も呼び出せる
}
};
.NET の generics は、コンパイル時にソースコードを要求しないという利点を求めた結果、多分、C++ ユーザにとって template を使う大きな理由の一つである「静的なダックタイピング」を切り捨てざるを得なかったのだろう。
これまで generic を template のようなものだと思っていたが、これではまるで別物だ。考えを改めなければならない。
- Newer: 法則に違反したっていいじゃない
- Older: Pimpl イディオムのお手軽な実装
コメントを表示する前に、このブログのオーナーによる承認が必要になることがあります。コメントが表示されない場合は、承認されるまでしばらくお待ちください。