VC.NET ~ CTS(Common Type System): valueとreference

前置き

CTS(Common Type System)は、CLRの中核を成す要素らしいのですが、カバーする範囲が広く、つかみ所がない印象です。全体像は置いておいて、ここでは、reference typeとvalue typeに絞って考えてみましょう。

本記事では、"value"と"value type"を区別して使っています。また、"object"にも、特別な意味を込めています。

本文

型(、及びデータ)

CTSでは、全ての型が、reference typeかvalue typeに分類されます。しかし、そもそも、型とは何でしょうか。型とはデータの属性みたいなものです。じゃぁデータとは? はい、CTSでは、データをvalueとobjectに分類します。

valueとは単純なbitの並びです。valueそのものから型を判別することはできません。例えばunsigned charのデータはbitが8つ並んだvalueです。8つのbitだけからは、それがcharなのかunsigned charなのか判別できません。valueの型は、valueの宣言文からコンパイラが判断します。

一方objectは、クラスのインスタンスです。メソッドを呼んだり、データそのものから型を判別することが可能です。

value type

unsigned charやintはvalue typeです。unsigned charやintのデータはvalueです。

Boolean、Char、Int32などもvalue typeです。これらのクラスのデータはobjectです。C++/CLIで、value typeなクラスを定義するには、value classかvalue structを使います。

value class Foo {
public:
  void foo() {}

private:
  int i;
};

value struct Bar {
  int i;
  void bar() {}
};

value typeなクラスは、処理効率を重視したい場面で使います。そのため、いくつか機能面での制約があります。

  • デフォルトコンストラクタを定義できない
  • デストラクタやファイナライザを定義できない
  • コピーコンストラクタを定義できない
  • 代入演算子をオーバーロードできない
  • 親クラスの指定は不可(ただしinterfaceの実装はOK)
  • sealed扱い(つまり派生クラスを定義できない)

※C++/CLIとC#では、classとstructの解釈が違うという点に注意して下さい。C++/CLIでのclassとstructの違いは、メンバのデフォルトのaccessibilityの違いとして表れます(classのメンバはデフォルトでprivate、structはpublic)。一方C#では、classとstructの違いが、reference typeかvalue typeかを決めます。

int i;

Foo f;
Console::WriteLine(f.GetType()->BaseType->ToString());  // System.ValueType

Bar b;
b.i = 100;
Bar^ bb = b;                                            // (A)
bb->i = 200;
Console::WriteLine(b.i);                                // 100

Console::WriteLine(IntPtr(&i).ToString("x"));           // 12f3d4
Console::WriteLine(IntPtr(&f).ToString("x"));           // 12f3e0
  • value typeなクラスは、暗黙裡にSystem::ValueTypeを継承します
  • 妙な話しですが、System::ValueType自身はreference classです
  • value typeなクラスのデータ(つまりobject)は、スタックに配置されます
  • ただしboxyingされると、スタック上のobjectは、マネージ・ヒープ上にコピーされます

上記の(A)では、bに対するハンドルを取得したことにより暗黙のboxingが行われています。つまりbbが指しているのは、マネージ・ヒープ上に作られたbの複製objectです。よって、bb->iを変更しても、b.iは変化しません。怖いですね。

enumもvalue typeです。標準C++では、クラス内部で使う定数をenumで定義することがありますが、そんなとき、C++/CLIではenum classを使います。

ref class Foo {
public:
  enum class Color {
    Red = 1,
    Green = 16,
    Blue = 256
  };

  [Flags] 
  enum class Status {
    L2Error = 1,
    L3Error = 2,
    L4Error = 4,
    L5Error = 8,
    AllError = L2Error | L3Error | L4Error | L5Error
  };

  Foo() {}
  void show(Color col, Status sta);
};

StatusにFlagsアトリビュートが付いているところに注意して下さい。enum値の論理和が使えるということです。

void Foo::show(Color col, Status sta)
{
  switch ( col ) {
    case Color::Red:
      Console::WriteLine("Red");
      break;
    case Color::Green:
      Console::WriteLine("Green");
      break;
    case Color::Blue:
      Console::WriteLine("Blue");
      break;
    default:
      break;
  }

  if ( (sta & Status::L2Error) == Status::L2Error ) Console::WriteLine("L2Error");
}

最後のif文を if ( sta & Status::L2Error ) と書くとコンパイルエラーになります。詰めが甘いな、MS。

reference type

System::ObjectやSystem::Stringはreference typeで、そのデータはobjectです。C++/CLIで、reference typeなクラスを定義するには、ref classかref structを使います。

ref class Foo {
public:
  void foo() {}

private:
  int i;
};

ref struct Bar {
  int i;
  void bar() {}
};

繰り返しになりますが、C#では、(C++/CLIとは異なり)classかstructかによって、reference typeかvalue typeかが決まります。

int i;

Foo f;
Console::WriteLine(f.GetType()->BaseType->ToString());  // System.Object

Bar b;
b.i = 100;
Bar^ bb = %b;
bb->i = 200;
Console::WriteLine(b.i);                                // 200

Console::WriteLine(IntPtr(&i).ToString("x"));           // 12f3dc
pin_ptr<int> p = &b.i;
Console::WriteLine(IntPtr(p).ToString("x"));            // 151c3b4
  • reference typeなクラスは、自動的にSystem::Objectを継承します
  • reference typeなクラスのデータは、マネージ・ヒープに配置されます
  • つまり、一見、スタック変数のように見えるfもbも、そのobjectはマネージ・ヒープにあります

最後に、どうやらネイティブポインタもreference typeに分類されるようです。で、そのデータは、valueのはず…。この辺を考え出すと訳が分からなくなります。

Boxing、Box化

boxingとは、スタック上のvalueやobjectを、マネージ・ヒープ上のobjectへ変換する処理のことです。boxingは暗黙に行われます。valueがboxingされると、同じ値をラップしたobjectがマネージ・ヒープに生成されます。intならInt32、unsigned charならByteといった具合に、valueの型に応じて、ラップするobjectの型が決まります。boxingにより、objectへのハンドルが手に入りますが、それはあくまでも、複製へのハンドルだということに注意して下さい。

int i = 100;
int^ h = i;
Console::WriteLine(h->GetType()->ToString());   // System.Int32
*h = 200;
Console::WriteLine(i);                          // 100
Console::WriteLine(h->Equals(200));             // True
i = *h;

同様に、スタック上のobjectがboxingされると、そのobjectの複製がマネージ・ヒープ上に生成されます。前述の、value typeのサンプルコードを参照して下さい。