次の方法で共有


値型セマンティクス

値型セマンティクスは、Visual C++ 2010 では C++ マネージ拡張から変更されています。

以下に、C++ マネージ拡張仕様で使用されている標準的な値型を示します。

__value struct V { int i; };
__gc struct R { V vr; };

マネージ拡張では、4 種類の値型の構文バリアントがありました (フォーム 2 と 3 は同じ意味を持っています)。

V v = { 0 };       // Form (1)
V *pv = 0;         // Form (2) an implicit form of (3)
V __gc *pvgc = 0;  // Form (3)
__box V* pvbx = 0; // Form (4) must be local 

継承仮想メソッドの呼び出し

Form (1) は、標準的な値オブジェクトで、ToString() のような継承仮想メソッドの呼び出しを試行する場合を除き、よく理解されています。 この例を次に示します。

v.ToString(); // error!

このメソッドを呼び出す場合は、V ではオーバーライドされないので、コンパイラによる基本クラスの関連仮想テーブルへのアクセスが必要になります。 値型は、その仮想テーブル (vptr) に関連ポインターを持たない正式なストレージなので、v をボックス化する必要があります。 マネージ拡張では、暗黙のボックス化はサポートされていないので、プログラマは次のように明示的に指定する必要があります。

__box( v )->ToString(); // Managed Extensions: note the arrow

このデザインの背後にある主な目的は教育です。値型にインスタンスを提供しない "コスト" について理解するよう、基になる機構をプログラマに明らかにすることを意図しています。 V で ToString のインスタンスを格納するには、ボックス化は必要ありません。

新しい構文では、基になるボックス化自体のコストは取り除かれていませんが、オブジェクトを明示的にボックス化する構文の複雑さは取り除かれています。

v.ToString(); // new syntax

V 内に ToString メソッドの明示的なインスタンスを提供しないコストについてはクラス デザイナーを惑わせる恐れがあります。 暗黙のボックス化が好まれる理由は、通常、クラス デザイナーは 1 人しか存在しない一方で、ユーザー数は無限で、ユーザーには V を修正し、煩わしい明示的なボックスを削除する権限がないからです。

値クラスに ToString のオーバーライド インスタンスを提供するかどうかは、これを使用する頻度と場所に従って決定する必要があります。 使用頻度が低い場合、この定義から得られる利点はほとんどありません。 同様に、パフォーマンスが不要な用途に呼び出しても、全般的なパフォーマンスは向上しません。 また、ボックス化された値に追跡ハンドルを保持でき、ハンドルがボックス化を必要としていない場合に呼び出します。

値クラスの既定コンストラクターの廃止

マネージ拡張と新しい構文の間の値型のもう 1 つの相違点として、既定コンストラクターのサポートが廃止されました。 これは、関連の既定コンストラクターを呼び出すことなく CLR で値型のインスタンスを作成できる機会が実行時にあるためです。 つまり、値型内で既定コンストラクターをサポートしようとするマネージ拡張での試みは実際面では保証できませんでした。 保証がないのであれば、アプリケーションで非確定的な状態で置いておくよりサポートを廃止する方がすっきりします。

これは、当初の予想より悪くありません。 これは、値型の各オブジェクトが自動的にゼロ設定されるからです (各型は既定値に初期化されます)。 このため、ローカル インスタンスのメンバーが未定義になることはありません。 その意味では、些細な既定コンストラクターを定義する能力がなくても失うものは何もありません。実際、CLR で実行した方がより効率的になります。

問題は、マネージ拡張のユーザーが重要な既定コンストラクターを定義する場合です。 これは、新しい構文にマップされていません。 コンストラクター内のコードは、ユーザーが明示的に呼び出す必要のある名前付き初期化メソッドに移行する必要があります。

一方、新しい構文の値型オブジェクトの宣言は変更されていません。 これのマイナス面は、値型がネイティブ型をラップするには次の点で不十分である点にあります。

  • 値型内でデストラクターはサポートされていません。 つまり、オブジェクトの有効期間の終了によってトリガーされるアクションの設定を自動化できません。

  • ネイティブ型は、ポインターとしてマネージ型に格納する以外に方法はありません。ネイティブ型は、引き続き、ネイティブ ヒープ上に割り当てられます。

小規模なネイティブ クラスを参照型ではなく値型にラップし、ヒープが二重割り当てされないようにしています。ネイティブ ヒープはネイティブ型を保持し、CLR ヒープはマネージ ラッパーを保持します。 ネイティブ クラスを値型にラップすることで、マネージ ヒープを避けることができますが、ネイティブ ヒープ メモリの回収を自動化する手段がなくなってしまいます。 参照型以外に、重要なネイティブ クラスをラップする実際的なマネージ型はありません。

内部ポインター

Form (2) と Form (3) は、ほとんどすべてをアドレス指定できます (つまり、マネージまたはネイティブのすべて)。 このため、たとえば、マネージ拡張では次のすべてが許可されています。

__value struct V { int i; };
__gc struct R { V vr; };

V v = { 0 };  // Form (1)
V *pv = 0;  // Form (2)
V __gc *pvgc = 0;  // Form (3)
__box V* pvbx = 0;  // Form (4)

R* r;

pv = &v;            // address a value type on the stack
pv = __nogc new V;  // address a value type on native heap
pv = pvgc;          // we are not sure what this addresses
pv = pvbx;          // address a boxed value type on managed heap
pv = &r->vr;        // an interior pointer to value type within a
                    //    reference type on the managed heap

V* では、ローカル ブロック内 (このため、ダングリングが可能)、グローバル スコープ、ネイティブ ヒープ内 (アドレス指定するオブジェクトが削除済みの場合など)、CLR ヒープ内 (このため、ガベージ コレクション時に再配置が必要な場合に追跡されます)、および CLR ヒープの参照オブジェクトの内部 (内部ポインターは、その呼び出し時に、透過的に追跡されます) で位置をアドレス指定できます。

マネージ拡張では、V* のネイティブな側面を分離する手段はありませんでした。つまり、オブジェクトまたはサブオブジェクトをマネージ ヒープにアドレス指定する可能性を何が処理するかについて包括的に処理されます。

新しい構文では、値型ポインターは 2 種類に分類されます。非 CLR ヒープ位置に限定の V* とマネージ ヒープ内のアドレスが見込まれますが、これに限定されない内部ポインターの interior_ptr<V> です。

// may not address within managed heap 
V *pv = 0; 

// may or may not address within managed heap
interior_ptr<V> pvgc = nullptr; 

マネージ拡張の Form (3) と Form (2) は、interior_ptr<V> にマップされます。 Form (4) はトラッキング ハンドルです。 マネージ ヒープ内にボックス化されているオブジェクト全体をアドレス指定します。 新しい構文では V^ に変換されます。

V^ pvbx = nullptr; // __box V* pvbx = 0;  

マネージ拡張の次の宣言は、新しい構文では内部ポインターにマップされます (これらは System 名前空間の値型です)。

Int32 *pi;   // => interior_ptr<Int32> pi;
Boolean *pb; // => interior_ptr<Boolean> pb;
E *pe;       // => interior_ptr<E> pe; // Enumeration

組み込み型はマネージ型と見なされませんが、System 名前空間の型に対してエイリアスとして機能します。 このため、次のマップは、マネージ拡張と新しい構文のいずれに対しても有効です。

int * pi;     // => int* pi;
int __gc * pi2; // => interior_ptr<int> pi2;

既存のプログラムで V* を変換する場合に最も保守的な戦略は、これを常に interior_ptr<V> に変換することです。 これは、マネージ拡張での処理方法です。 新しい構文では、プログラマは、内部ポインターの代わりに V* を指定して値型を非マネージ ヒープ アドレスに制限するオプションを利用できます。 プログラムの変換時に、その使用のすべての推移的閉包が可能で、割り当てアドレスがマネージ ヒープ内に存在しない場合、V* のまま残しても問題ありません。

固定ポインター

ガベージ コレクターでは、通常は圧縮フェーズ中に、CLR ヒープ上に存在するオブジェクトをヒープ内の別の場所に移動する場合があります。 この移動は、追跡ハンドル、追跡参照、およびこれらのエンティティを透過的に更新する内部ポインターにとって問題になりません。 ただし、ユーザーが CLR ヒープ上のオブジェクトのアドレスをランタイム環境の外部に送っている場合は問題になります。 この場合、オブジェクトの一時的な移動によりランタイム エラーが発生する場合があります。 これらのオブジェクトが移動しないように、外部使用の範囲において場所を固定する必要があります。

マネージ拡張では、__pin キーワードでポインター宣言を修飾することで固定ポインターを宣言します。 次に、マネージ拡張仕様からわずかに変更された例を示します。

__gc struct H { int j; };

int main() 
{
   H * h = new H;
   int __pin * k = & h -> j;
  
   // …
};

新しい言語デザインでは、内部ポインターの構文に似た構文を使用して固定ポインターを宣言します。

ref struct H
{
public:
   int j;
};

int main()
{
   H^ h = gcnew H;
   pin_ptr<int> k = &h->j;

   // …
}

新しい構文における固定ポインターは、内部ポインターの特殊な例です。 固定ポインターに対する元の制限は残っています。 たとえば、パラメーターやメソッドの戻り値の型として使用できず、ローカル オブジェクト上でしか宣言できません。 新しい構文では、制限がさらに追加されています。

固定ポインターの既定値は、0 ではなく nullptr です。 pin_ptr<> を初期化したり、0 を割り当てたりすることはできません。 既存のコードに 0 を割り当てるには、nullptr に変更する必要があります。

マネージ拡張では、固定ポインターを使用してオブジェクト全体の位置をアドレス指定できます。マネージ拡張仕様から次の例を示します。

__gc class G {
public:
   void incr(int* pi) { pi += 1; }
};
__gc struct H { int j; };
void f( G * g ) {
   H __pin * pH = new H;   
   g->incr(& pH -> j);   
};

新しい構文では、new 式から返されるオブジェクト全体を固定できません。 さらに、内部メンバーのアドレスを固定する必要があります。 次に例を示します。

ref class G {
public:
   void incr(int* pi) { *pi += 1; }
};
ref struct H { int j; };
void f( G^ g ) {
   H ^ph = gcnew H;
   Console::WriteLine(ph->j);
   pin_ptr<int> pj = &ph->j;
   g->incr(  pj );
   Console::WriteLine(ph->j);
}

参照

参照

Classes and Structs (Managed)

interior_ptr

pin_ptr

概念

値型とその動作