連載:深入り.NETプログラミング

.NETと安全なポインタ

NyaRuRu
Microsoft MVP Windows - DirectX(Jan 2004 - Dec 2009)
2009/01/27
Page1 Page2

型付けされた参照

 管理下ポインタによく似たものに、「型付けされた参照」がある。ボックス化を伴わずに、任意の型の値型を参照渡しできる、といえばその特徴が伝わるだろうか。型付けされた参照を利用すれば、正当性検証可能なsscanf関数(=C言語の文字列書式化関数。関数引数として複数の型を受け取れる)を実装することができる。

 とはいえ、型付けされた参照はCLS(共通言語仕様)準拠ではなく、今後C#で正式にサポートされる可能性も低い。大多数の人にとって、いままでもこれからも、型付けされた参照を目にする機会はないだろう。ただし、Visual C#には非公式キーワードとして__makeref、__refvalue、__arglistがあって、これらを使用すれば対応するILコードを出力することはできる。

検証可能なByRef(参照型)の戻り値

 C++言語では、関数戻り値の型に「(C++の意味での)参照型」を許している。この参照型の戻り値は、ユーザー定義コンテナの振る舞いを組み込み配列と同じにしたいときに活躍する。

std::vector<int> vec;
vec.push_back(0);
vec.push_back(1);
vec.push_back(4);

// (C++の意味での)参照型の戻り値
vec[1] = -1;
参照型の戻り値が活用されているコード例(C++)
このコードは、標準テンプレート・ライブラリ(STL)のベクタ(=ユーザー定義コンテナ)を作成して、int型の数値を要素として挿入している。最後の行は、参照型の戻り値が活用されているので、int型の配列のように扱うことが可能になっている。

 しかしC#のメソッドやプロパティ、インデクサ全般で管理下ポインタを戻り値に使えない。これはCLI仕様の制限ではなく、C#という言語設計に基づく制限である。CLI規格上は正当性検証可能な形で管理下ポインタを戻り値にする方法がある。

 配列型が持つインスタンス・メソッドのAddressはその例である。IntelliSenseには表示されないが、任意の配列型“T[]”は、Addressというインスタンス・メソッドを持っている。このメソッドは指定されたインデックスに対応する要素の管理下ポインタを返す。

Console.WriteLine(typeof(string[]).GetMethod("Address"));

// 結果
// System.String& Address(Int32)
配列型の持つ隠しメソッド「Address」をリフレクションで取得する方法
Addressメソッドの戻り値は、配列要素を指す管理下ポインタである。

 C++/CLIでは追跡参照を用いてこのようなメソッドを定義することができる。実際、STL/CLRには配列のように使えるコンテナ・クラスが存在する。

 ただし、.NET 1.x時代には管理下ポインタを戻り値とするケースについてあまり考慮されていなかったようだ。.NET 1.x時代に提供されていたMicrosoftのコード検証器は、管理下ポインタを戻り値とするILコードを一律に正当性検証不能としていたらしい(配列のAddressメソッドが問題にならなかったのは、CLRが提供する特殊メソッドだったからと考えられる)。

 [参考]

 Joe Duffy's Weblog: Verifiable ByRef returns?

 スタック領域をポイントする管理下ポインタを戻り値とするILは、正当性検証可能ではない(戻り値以外のケースでは、「tail.」プレフィックス*1が付いた呼び出しで考慮すべき事項が存在する)。それ以外、静的フィールドや、GCヒープをポイントする管理下ポインタの返却は“安全”である。Microsoftのコード検証器は、.NET 2.0以降でこの違いを区別するようになった。

*1 中間言語(IL)では、「tail.」をプレフィックスとしてcall命令の直前に付けると、末尾最適化(=再帰処理を末尾で行うように最適化すること)が行われる。

Controlled-mutability managed pointer

 ジェネリクスを導入するに当たり、値型に対する操作と参照型に対する操作を同じILコードで表現したいということになった。

 ジェネリクス以前のおさらいから始めよう。ジェネリクス導入前の.NETでは、値型と参照型でメソッド呼び出しの方法が異なっていた。

  • 値型のインスタンス・メソッドを呼び出すときには、thisポインタとして管理下ポインタを評価スタックにプッシュしてからメソッドを呼び出す

  • 参照型のインスタンス・メソッドを呼び出すときは、thisポインタとしてオブジェクト参照のコピーを評価スタックにプッシュしてからメソッドを呼び出す

 値型の場合はオブジェクトそのものではなくそれを指す管理下ポインタをプッシュし、参照型では(すでにポインタ相当なので)コピーをプッシュするわけだ。この非対称性を統一する必要がある。

 Microsoftは、(ILの)新しいプレフィックス「constrained」の追加によってこれに対処した。「constrained」はcallvirt命令の前にのみ許されるプレフィックスである。

 「constrained X」という制約を受けたcallvirt命令(以下「constrained. callvirt命令」と書く)は、Xが値型か参照型かによって動作を変える。その動作は次のようなものだ。

  • Xが値型のとき: 評価スタックには管理下ポインタが入っていると見なし、その管理下ポインタをthisに採用する*2

  • Xが参照型のとき: 評価スタックにはオブジェクト参照を格納した領域の管理下ポインタが入っているとし、それを逆参照してからthisに採用する

*2 これは値型のボックス化回避という点でも重要である。ただし原理上ボックス化が回避できない場合もある(System.Object、System.ValueType、System.Enumに定義され、かつオーバーライドされていメソッドを呼び出すとき)。「constrained. callvirt命令」ではこの非対称性も吸収され、必要ならば内部で自動的にボックス化処理が行われる。

 つまり、IL側では値型/参照型にかかわらず、常に管理下ポインタをプッシュするようにしておき、.NET VM(CLRに限らず、任意のCLI実装)側で帳尻を合わせることにしたのだ。これで値型に対するメソッド呼び出しと参照型に対するメソッド呼び出しを同じILコードで表現できるようになった……かに見えた。

 配列が問題であった。

 問題は、配列要素を指す管理下ポインタの取得にあった。値型配列では特に問題はない。しかし、ある理由によって、参照型配列の要素を指す管理下ポインタの取得には型チェックが存在し、その型チェックが若干厳しすぎるという問題があったのだ。

 問題は.NET登場時のJava言語への対応にさかのぼる。

 Javaの型システムは、いわゆる配列の共変性(下記のコードを参照)をサポートした。この型システムは、配列要素への代入時に余分な型検査が必要になるという欠点を持つ。そしてJavaから.NETへの移植を容易にしたいという理由で、参照型の配列に限り、配列の共変性はCLI仕様にも引き継がれた。

class Base {}
class Deriv : Base{}
class Deriv2 : Base{}

public static class Program
{
  public static void Main()
  {
    Deriv[] derives = new Deriv[10];

    // Javaと同様、これは許される(配列の共変性)
    Base[] bases = derives;

    // これはコンパイル時にエラーとして排除できる
    derives[0] = new Deriv2();

    // これはコンパイル・エラーにならない(正しいCLI実装であれば実行時エラーを発生させる)
    bases[0] = new Deriv2();
  }
}
「配列の共変性」の問題を示すサンプル・プログラム(C#)
型安全性を維持するためには、最後の代入文はエラーとならなければいけない。
このチェックのために、余分な実行時型チェックが必要になる。

 これを管理下ポインタに拡張するとどうなるか?

class Base {  public void BaseMethod() { } }
class Deriv : Base{}
class Deriv2 : Base{}

public static class Program
{
  public static void Foo1(ref Base obj)
  {
    obj = new Deriv2();
  }

  public static void Foo2(ref Base obj)
  {
    obj.BaseMethod();
  }

  public static void Main()
  {
    Deriv[] derives = new Deriv[10];

    // Javaと同様、これは許される(配列の共変性)
    Base[] bases = derives;

    // これはコンパイル時にエラーとして排除できる
    derives[0] = new Deriv2();

    // これはコンパイル・エラーにならない(CLRが実行時エラーを検出する)
    bases[0] = new Deriv2();

    // 型安全性を保つには、Foo1メソッド内の代入が完了するまでに
    // エラーが発生しなければならない
    Foo1(ref bases[0]);

    // Foo2メソッドの処理内容は、最後まで実行しても本来問題ない
    Foo2(ref bases[0]);
  }
}
「配列の共変性」の問題を管理下ポインタに拡張したサンプル・プログラム(C#)

 型安全性のためには、Foo1メソッドの代入処理は完了してはならない。代入完了までのどこかで例外を発生させる必要がある。

 .NETが当初採用した方法は、参照型の配列要素を指す管理下ポインタを取得するときに、あえて厳しめの型チェックを行い、Foo1メソッドの中で余分な実行時型検査を行わなくて済むようにするというものだ。実際の型がDeriv[]型の配列要素からBase&型の管理下ポインタを作成しようとすると、その時点で例外が発生する。つまり、上記のコードでは「ref bases[0]」の段階で例外が発生し、Foo1メソッドの内部まで実行されない。

 しかしこの方法にはデメリットもある。bases[0].BaseMethod()をconstrained. callvirt命令で呼び出すときのことを考えてみてほしい。これはBase&型の管理下ポインタを必要とするが、上記理由により管理下ポインタを取得しようとしただけで例外が発生してしまう。Foo2同様、本来これは静的な検査で型安全性が確かめられるにもかかわらず、おおざっぱな型チェックによってエラーとなっているのである。

 結局.NET 2.0は管理下ポインタを再分類した。新設された管理下ポインタのサブセットは、「controlled-mutability managed pointer」と呼ばれる(以下CMMPと書く)。既存コードの意味を変えないために、配列要素を指すCMMPを取得するための「readonly.」プレフィックスが新設された。

 CMMPに許される操作は以下のとおりである。これらの操作に限っては、余分な実行時型チェックが不要、というわけだ。

  1. ldfld、ldflda、stfld、call、callvirt、constrained. callvirtの各命令でthisポインタとして使用する
  2. ldind.*もしくはldobjなどの各命令でスタックに読み込むために使用する
  3. cpobj命令のソース・パラメータとして使用する

 これ以外の操作や命令、例えばstobj、stind.*、initobj、mkrefanyにCMMPを使用するコードは正当性検証可能ではない。

 CMMPと通常の管理下ポインタの間にメタデータ的な違いは存在しない。もしCMMPにメタデータが与えられていれば、先ほどのFoo2の引数や、C# 3.0で導入された拡張メソッドの第1引数はCMMPがふさわしいのだが、現実にはそうならなかったことが残念である。現在のCLI仕様では、CMMPはIL内部でのみ意味を持ち、ILのフロー解析を通してのみ決定される。フローでCMMPと管理下ポインタが合流すると、CMMPとして扱われる。

まとめ

 今回は管理下ポインタを取り上げ、管理下ポインタがどのように.NETプログラミングを支えているかを紹介した。ref/outキーワードによる参照渡しだけでなく、値型オブジェクトのフィールド・アクセスやメソッド呼び出しでも管理下ポインタが活躍しているのは案外知られていないのではないかと思う。

 また、JVMの置き土産によって、ジェネリックな配列の扱いがややこしくなってしまった事情も紹介した。値型や管理下ポインタ、ジェネリクスは.NET VM(=CLR)とJVMとの大きな差別化要因であり、これを機会に.NET VMへの理解が深まれば幸いである。End of Article


 INDEX
  [連載]深入り.NETプログラミング
  .NETと安全なポインタ
    1.管理下ポインタ(Managed Pointer)の特徴と具体例
  2.管理下ポインタによって支えられている.NETの値型

インデックス・ページヘ  「深入り.NETプログラミング」


Insider.NET フォーラム 新着記事
  • 第2回 簡潔なコーディングのために (2017/7/26)
     ラムダ式で記述できるメンバの増加、throw式、out変数、タプルなど、C# 7には以前よりもコードを簡潔に記述できるような機能が導入されている
  • 第1回 Visual Studio Codeデバッグの基礎知識 (2017/7/21)
     Node.jsプログラムをデバッグしながら、Visual Studio Codeに統合されているデバッグ機能の基本の「キ」をマスターしよう
  • 第1回 明瞭なコーディングのために (2017/7/19)
     C# 7で追加された新機能の中から、「数値リテラル構文の改善」と「ローカル関数」を紹介する。これらは分かりやすいコードを記述するのに使える
  • Presentation Translator (2017/7/18)
     Presentation TranslatorはPowerPoint用のアドイン。プレゼンテーション時の字幕の付加や、多言語での質疑応答、スライドの翻訳を行える
@ITメールマガジン 新着情報やスタッフのコラムがメールで届きます(無料)

注目のテーマ

Insider.NET 記事ランキング

本日 月間