VC.NET ~ C++/CLIマイナスC++

前置き

C++/CLIは、標準C++をベースに、CLR(CLI)アセンブリを記述するために必要な機能を追加したプログラミング言語です。C#やVisual BasicでもCLRアセンブリは書けるのですが、C++ Interopという仕組みを使える点がC++/CLIの強みです。

C++/CLIと標準C++は、どれくらい違うのか。これはもう、かなり違います。CとC++くらい違うんじゃないでしょうか。包括的な情報は、MSDNのここら辺に整理されていますが、本記事では、個人的に避けて通れないと感じるトピックを紹介します。

本文

GCとマネージ・ヒープ

CLRで導入されたGC(ガベッジコレクション)に関連する機能が追加されました。gcnewやref class、ハンドル、追跡参照などです。詳しくは、この記事を参照して下さい。

value typeとreference type、及びBoxing

CLRでは、value typeとreference typeの違いを意識することが重要です。これに関して、C++/CLIでは、value class、value struct、ref class、ref struct、enum class、enum structなどの予約語が導入されました。詳しくは、この記事を参照して下さい。

3種類のポインタ

interior_ptrとpin_ptrとIntPtrのことです。

ディスポーズとファイナライズ

この記事で詳しく説明しています。

safe_cast、Cスタイルのキャスト

詳しくは、こちらの記事で。

デリゲートとイベント

デリゲートは、標準C/C++の関数ポインタに代わる機能です。マネージなクラスのメソッド向けの関数ポインタってことですが、複数の関数ポインタをチェーンさせて、連続してコールバックできるところが、従来の関数ポインタからの強化ポイントです。

まず、関数ポインタのプロトタイプ(シグニチャ)に相当するものを宣言します。

delegate bool IsEqualDele(String^ s1, String^ s2);

このプロトタイプにマッチするメソッドを持ったクラスを定義します。

ref class CmpLikeStr {
public:
  static bool eq(String^ s1, String^ s2) { return s1 == s2; }
};

ref class CmpLikeHex {
public:
  bool equal(String^ s1, String^ s2) {
    return Convert::ToInt32(s1, 16) == Convert::ToInt32(s2, 16);
  }
};

デリゲートを利用する関数を書いておきます。いわゆる高階関数ってことですかね。

void comp(IsEqualDele^ dele)
{
  Console::WriteLine(dele("ab", "0xAB") ? "same" : "different");
}

じゃぁ、デリゲートをgcnewして、comp()に突っ込んでみましょう。

comp(gcnew IsEqualDele(&CmpLikeStr::eq));           // different

CmpLikeHex^ clh = gcnew CmpLikeHex();
comp(gcnew IsEqualDele(clh, &CmpLikeHex::equal));   // same

デリゲートにしたいメソッドがクラスメソッドの場合とインスタンスメソッドの場合とで、生成方法が違うという点に注意して下さい。

また、同じメソッドのデリゲートは何度gcnewしても、等価なObjectになります。

Console::WriteLine(gcnew IsEqualDele(&CmpLikeStr::eq)
  == gcnew IsEqualDele(&CmpLikeStr::eq) ? "YES" : "NO");             // YES
Console::WriteLine(gcnew Object() == gcnew Object() ? "YES" : "NO"); // NO

複数のメソッドをチェーンさせて、まとめてコールバックする例も示しておきます。ナンセンスな例ですが…。

IsEqualDele^ d = gcnew IsEqualDele(&CmpLikeStr::eq);

CmpLikeHex^ clh = gcnew CmpLikeHex();
d += gcnew IsEqualDele(clh, &CmpLikeHex::equal);    // warning C4358

comp(d);           // same

+= 演算子でチェーンさせているわけですが、return値を持つメソッドをチェーンさせるとwaningが出ます。デリゲートをコールバックしたとき、チェーンされたメソッドのうち、どのメソッドのreturn値がデリゲートの最終的なreturn値になるかは不定のようです。上記の例では、最後に呼ばれたメソッドのreturn値が採用されているようですが…。

ちなみに、メソッドがコールバックされる順番は、Delegate::GetInvocationList()で調べることができます。かなり苦しい例ですが…。

array<Delegate^>^ list = d->GetInvocationList();
if ( list->Length > 0 &&
     list[0] == gcnew IsEqualDele(&CmpLikeStr::eq) ) {
  Console::WriteLine("CmpLikeStr is #1");
}

チェーンから削除するには -= 演算子を使います。

d -= gcnew IsEqualDele(clh, &CmpLikeHex::equal);    // ここでもC4358!!

さて、デリゲートの次は、イベントです。イベントは、デリゲートを外部のクラスに公開するときに使います。単純にデリゲートをpublicにすると、外部からデリゲートを好き放題に操作(例えばコールバックとか)されてしまいますが、イベントなら、外部からの操作をメソッドの登録(+=)と削除(-=)に制限しつつ公開することができます。Observer(JavaのListener)的なデザインパターンを実装したいときなどにピッタリです。

例を示します。ポイントは(A)~(E)です。

delegate void HungryEventHandler(int level);     // (A)

ref class Obserbable {
public:
  event HungryEventHandler^ onHungry;            // (B)

  void run() {
    mLevel = 0;
    for (;;) {
      Sleep(100);
      onHungry(mLevel);                          // (C)
      mLevel += 4;
      if ( mLevel >= 20 ) break;
    }
  }

  void drink() { mLevel--; if ( mLevel < 0 ) mLevel = 0; Console::WriteLine("drink"); }
  void food() { mLevel -= 2; if ( mLevel < 0 ) mLevel = 0; Console::WriteLine("food"); }

private:
  int mLevel;
};

ref class Obserber1 {
public:
  Obserber1(Obserbable^ ob) {
    mOb = ob;
    ob->onHungry += gcnew HungryEventHandler(this, &Obserber1::h1);
                                                 // (D)
  }

private:
  Obserbable^ mOb;
  void h1(int level) {
	if ( (level % 3) ) mOb->drink();
  }
};
  
ref class Obserber2 {
public:
  Obserber2(Obserbable^ ob) {
    mOb = ob;
    ob->onHungry += gcnew HungryEventHandler(this, &Obserber2::h2);
                                                 // (E)
  }

private:
  Obserbable^ mOb;
  void h2(int level) {
	if ( level == 16 ) mOb->food();
  }
};

では、使ってみましょう。

Obserbable ob;
Obserber1 ob1(%ob);
Obserber2 ob2(%ob);
ob.run();              // drink
                       // drink
                       // drink
                       // drink
                       // drink
                       // food
                       // drink

プロパティ

オブジェクト指向言語にとって、プロパティという機能をサポートするのが最近の流行なんでしょうか。C++/CLIもプロパティをサポートしています。

ref class Bar {
public:
  Bar(String^ n) { mName = n; }
  property String^ name {
    String^ get() { return mName; }
    void set(String^ value) { mName = value; }
  }

  property int age;

private:
  String^ mName;
};

(A)は、getterとsetterを明示的に実装した例です。オーソドックスなパターンですね。実装するのは、getterとsetterのどちらか一方だけでも構いません。実装の中身も自由です。

(B)は簡易表記の例です。この場合、(A)のようなオーソドックスなgetterとsetterが自動的に実装されます。ageプロパティの値を保持するためのメンバ変数(nameプロパティのmNameに相当するもの)も暗黙裡に定義されます。

Bar b("Bar");
b.age = 25;
Console::WriteLine(b.name);  // Bar

abstract、override、sealed、new

標準C++では、派生クラスによるメソッドのオーバーライドを基本クラス側で抑止することができません。そのため、virtualでないメソッドを派生クラスがオーバーライドしてしまい、ポリモーフィズム(多態性)が働かなくて悩んだりすることがあります。C++/CLIでは、こういった問題を改善するため、クラスの設計意図を派生クラスに明確に伝えるための仕様が導入されています。

  • virtualでないメソッドはオーバーライドできない
  • virtualでも、sealedキーワード付きのメソッドはオーバーライドできない
  • オーバーライドするときは、overrideキーワードを付ける
  • オーバーライドするが、ポリモーフィズムを使いたくない場合はnewキーワードを付ける
  • 抽象基本クラスにはabstractキーワードを付ける
  • 純粋仮想メソッドを持つクラスには、必ずabstractキーワードを付けなければならない
  • sealedキーワードが付いたクラスを継承することはできない
ref class B1 {
public:
  B1() {}

protected:
  void m1() {}                   // オーバーライド禁止
  virtual void m2() {}           // オーバーライド可能
  virtual void m3() {}           // オーバーライド可能
  virtual void m4() {}           // オーバーライド可能
  virtual void m5() sealed {}    // オーバーライド禁止
};

ref class B2 abstract {          // 抽象基本クラス
public:
  B2() {}

protected:
  virtual void m1() {}           // オーバーライド可能
  virtual void m3() sealed {}    // オーバーライド禁止
};

ref class D : B1 {
public:
  D() {}

protected:
  virtual void m2() override {}         // オーバーライド
  virtual void m3() override sealed {}  // オーバーライドしつつ封印
  virtual void m4() new {}              // オーバーライドするが多態は無し
};

Accessibilities/Visibilities

標準C++と同様、クラスやメソッドにprivateやpublicキーワードを付けて可視性を制御することができます。しかもC++/CLIでは、アセンブリの外向けと中向けとで可視性を変えることができます。

ref class Foo {
public protected:    // アセンブリ外のクラスにはprotected、アセンブリ内にはpublic
  Foo() {}

internal:            // アセンブリ外のクラスにはprivate、アセンブリ内にはpublic
  void show() {}

protected private:   // アセンブリ外のクラスにはprivate、アセンブリ内にはprotected
  int b;
};

public protectedは、protected publicと書いても同じ意味です。同様に、protected privateはprivate protectedと同じです。

for each

C++/CLIでは、待望のfor each構文が導入されました。コンテナ系の(厳密に言えばIEnumerableを継承した)クラスに対して使うことができます。

String^ msg = "SECRET";
for each ( Char c in msg ) Console::Write(Char::ToLower(c));
Console::WriteLine("");      // secret

finally

C++/CLIでは、例外(try-catch)構文に、finally節を書くことができます。標準C++には無いんでしたっけね?

try {
  ...
}
catch ( Exception^ ex ) {
  ...
}
finally {
  ...
}

literalとinitionly

クラス内で定数を使いたい場合、標準C++なら、enumやconst定数、#defineあたりを使うと思いますが、C++/CLIではliteralやinitonlyを使います(enumでもいいけど)。

literalは、コンパイル時にその値と置き換えられる、という点で、標準C++のconst定数や#defineに近いと思います。

ref class Foo {
public:
  Foo() { mBuf = gcnew array<unsigned char>(BUF_SIZE); }

private:
  literal int BUF_SIZE = 1024;
  literal String^ MSG = "error";
  array<unsigned char>^ mBuf;
};

配列をliteral宣言することはできません。また、ハンドルをliteral宣言してgcnewで初期化するといった書き方も許されていません。

literalを適用できない型のデータを定数として使いたい場合のために、initonlyキーワードがあります。initonly宣言された変数は、初期化すると、それ以降は変更することができません。また、初期化するタイミングも限定されています。

  • initonly staticな変数は、宣言時か、staticコンストラクタ内で初期化
  • initonlyな変数は、コンストラクタ内で初期化
ref class Foo {
public:
  Foo() {
    MSG_TABLE = gcnew array<String^>{ "Hi", "Hey", "Yo", "Hello", "Bye" };
    DateTime now = DateTime::Now;
    CREATE_TIME = now.ToString();
  }

private:
  initonly static int NUM_MSGS = 5;
  initonly array<String^>^ MSG_TABLE;
  initonly String^ CREATE_TIME;
};

array

C++/CLIでは、Genericsを使った配列表記が導入されました。この表記を使う配列は全て、System::Arrayクラスを継承するので、Arrayクラスの様々なメソッドやプロパティを使うことができます。

array<int^>^ a = { 1, 2, 3 };
Console::WriteLine(a->Length);         // 3
for each ( int i in a ) Console::Write(i);
Console::WriteLine("");                // 123

typeid

実行時にオブジェクトの型を調べるための仕組みを、一般に、RTTI(Run-Time Type Information、実行時型情報)と呼びます。.NETでも、System::Object::GetType()により型情報を知ることができます。

C++/CLIではさらに、typeidキーワードによってコンパイル時に型情報を知ることができます。

Console::WriteLine(String::typeid);              // System.String
Console::WriteLine(array<String^>::typeid);      // System.String[]
Console::WriteLine(int::typeid);                 // System.Int32
if ( int::typeid == System::Type::GetType("System.Int32") ) Console::WriteLine("YES");
                                                 // YES
Last modified:2012/03/09 16:44:12
Keyword(s):
References:[.NETアプリ開発]
This page is frozen.