Home > .NET Framework > | C++/CLI > 仮想関数を generic にする

仮想関数を generic にする

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 のようなものだと思っていたが、これではまるで別物だ。考えを改めなければならない。

Comments:0

コメントを表示する前に、このブログのオーナーによる承認が必要になることがあります。コメントが表示されない場合は、承認されるまでしばらくお待ちください。

Comment Form

Home > .NET Framework > | C++/CLI > 仮想関数を generic にする

Search
Feeds

Return to page top