Const と Invariant
データ構造やインターフェイスを調べるときには、 どのデータが不変でどのデータが可変で、変更を行う可能性があるのは誰か、 といった情報が簡単にわかると非常に便利です。 そしてこれは、言語の型システムの助けによって実現することができます。 データは const あるいは invariant とマーク付けすることができ、 デフォルトでは変更可能 (mutable) となっています。
invariant は、 一度構築されたら決して変更されることのないデータに適用します。 invariant なデータ値は、 プログラムの実行中一貫して同じ値であり続けます。 invariant なデータは ROM (Read Only Memory) や、ハードウェアによって読み取り専用とされたメモリページに配置することができます。 invariant なデータは決して変更されないので、 さまざまなプログラム最適化の機会をもたらします。また、 アプリケーションをより関数型のプログラミングスタイルで書くことを可能にします。
const は、そのconst参照を通しては変更を加えることのないデータに適用します。 ただし、データ自体の値は、 同じデータを指す別の参照経由で変化するかもしれません。 アプリケーション内で const を使用することで、 渡されたデータを変更せずに読み取りだけであるというインターフェイスを表現することができます。
invariant と const のどちらも、推移的です。つまり、 invariant な参照経由でアクセスできるデータは全てinvariantですし、 const の場合にも同様のことが成り立ちます。
invariant 記憶域クラス
一番単純な invariant は、記憶域クラスとしての使い方です。 これは記号定数(manifest constant)を宣言するのに使用することができます。
invariant int x = 3; // x は 3 になる x = 4; // エラー。x は invariant char[x] s; // s は char 3つの配列
invariant変数の型は初期化子から推論されます:
invariant y = 4; // int型のy y = 5; // えらー。y は invariant
初期化子を与えていない場合、 invariant を対応するコンストラクタで初期化することが可能です:
invariant int z; void test() { z = 3; // エラー。z は invariant } static this() { z = 3; // ok // 静的初期化子のないinvariantの値は設定できる }
非ローカルな invariant 宣言の初期化子はコンパイル時評価可能でなければなりません:
int foo(int f) { return f * 3; } int i = 5; invariant x = 3 * 4; // ok, 12 invariant y = i + 1; // エラー。コンパイル辞評価不可能 invariant z = foo(2) + 1; // ok。 foo(2) はコンパイル時に7に評価される
staticでないローカルなinvariant宣言の初期化子は、 実行時に評価されます:
int foo(int f) { invariant x = f + 1; // 実行時に評価 x = 3; // エラー。x は invariant }
invariant は推移的なので、 invariantから参照されているデータもまたinvariantです:
invariant char[] s = "foo"; s[0] = 'a'; // エラー。s は invariant なデータを指している s = "bar"; // エラー。s は invariant
invariant宣言はlvalueとして使うことができます。 つまり、アドレスを取ることが可能です。
const 記憶域クラス
const 宣言は、以下の違いを除いて、 invariant とほぼ同じです:
- const宣言された変数を通してデータを書き換えることはできないが、 同じデータを参照する別の箇所がデータを書き換えることは あるかもしれない
- const宣言された変数の型は、const
invariant 型
invariantと修飾された型の値は、決して変更されません。 予約語 invariant は type constructor として使うことができます:
invariant(char)[] s = "hello";
invarinatは、括弧の中に書かれた型に適用されます。 例えばこの例の場合、変数 s に別の値を代入することはできますが、 s[] の中身に別の値を入れることはできません:
s[0] = 'b'; // エラー。s[] は invariant s = null; // ok。sそのものはinvariantでない
invariant性は推移的です。つまり、invariant性は、 invariant型から参照されるすべてのものに伝搬します:
invariant(char*)** p = ...; p = ...; // ok, p はinvariantでない *p = ...; // ok, *p はinvariantでない **p = ...; // エラー。 **p は invariant ***p = ...; // エラー。***p は invariant
記憶域クラスとして使われるinvariantは、 宣言の型全体をinvariantで修飾したのと同じ効果になります:
invariant int x = 3; // x は invariant(int) 型 invariant(int) y = 3; // y は invariant
invariant なデータを作る
一番単純な方法は、最初からinvariantとされているリテラルを使うことです。 例えば、文字列リテラルは invarinat です。
auto s = "hello"; // s は invariant(char)[5] char[] p = "world"; // エラー。暗黙キャストで // invariantを外すことはできない
二つめの方法は、データを明示的にinvariantにキャストすることです。 この方法をとるときは、 そのデータを他に書き換えるような参照がないことをプログラマが保証する必要があります。
char[] s = ...; invariant(char)[] p = cast(invariant)s; // 未定義動作 invariant(char)[] p = cast(invariant)s.dup; // ok。pがs.dupへの唯一の参照
.idup プロパティを使うと、 配列のinvariantなコピーを簡単に作ることができます:
auto p = s.idup; p[0] = ...; // エラー。p[] は invariant
キャストでinvariantを取り除く
invariant をキャストで除去することは可能です:
invariant int* p = ...; int* q = cast(int*)p;
しかし、だからといって、データを書き換えられるようになるわけではありません:
*q = 3; // コンパイルは通るが、未定義動作
invariant性を取り除くキャストは、正しく静的型がついていないくて、 しかもそれが修正できないという場面で必要となってしまうことがあります。 例えば、正しく型のついていないライブラリを使う場合などです。 キャストは使い方次第で毒にも薬にもなる道具です。 コンパイラが保証するinvariant性の正しさをキャストで取り除く以上、 データの invariant 性に関してプログラマが責任を持つことが前提となります。
invariant メンバ関数
invariant メンバ関数では、 this参照経由で参照される全てのデータがinvariantであることが保証されます。 以下のように宣言します:
struct S { int x; invariant void foo() { x = 4; // エラー。 x は invariant this.x = 4; // エラー。x は invariant } }
const 型
const 型は invariant 型に似ていますが、const は データの読み込み専用の view を表します。 他に参照があってデータを書き換える可能性は残っています。
const メンバ関数
const メンバ関数は、 thisの指すオブジェクト自身のメンバを書き換えることが許されない メンバ関数です。
暗黙の変換
書き換え可能な型とinvariant型のデータは、constに暗黙変換できます。 書き換え可能な型をinvariantに変換したり、 その逆の変換が暗黙に行われることはありません。
Dのinvariant,const と C++のconstの比較
機能 | D | C++98 |
---|---|---|
予約語 const | Yes | Yes |
予約語 invariant | Yes | No |
constの記法 | 関数的:
// const int への const ポインタ へのポインタ const(int*)* p; |
後置:
// const int への const ポインタ へのポインタ const int *const *p; |
推移的 const | Yes:
// const intへのconstポインタへのconstポインタ const int** p; **p = 3; // エラー |
No:
// intへのポインタへのconstポインタ int** const p; **p = 3; // ok |
const除去キャスト | Yes:
// const intへのポインタ const(int)* p; int* q = cast(int*)p; // ok |
Yes:
// const intへのポインタ const int* p; int* q = const_cast<int*>p; //ok |
const除去後の書き換え | No:
// const intへのポインタ const(int)* p; int* q = cast(int*)p; *q = 3; // 未定義動作 |
Yes:
// const intへのポインタ const int* p; int* q = const_cast<int*>p; *q = 3; // ok |
トップレベルのconstでのoverload | Yes:
void foo(int x); void foo(const int x); // ok |
No:
void foo(int x); void foo(const int x); // エラー |
constとmutableの別名参照 | Yes:
void foo(const int* x, int* y) { bar(*x); // bar(3) *y = 4; bar(*x); // bar(4) } ... int i = 3; foo(&i, &i); |
Yes:
void foo(const int* x, int* y) { bar(*x); // bar(3) *y = 4; bar(*x); // bar(4) } ... int i = 3; foo(&i, &i); |
invariantとmutableの別名参照 | Yes:
void foo(invariant int* x, int* y) { bar(*x); // bar(3) *y = 4; // 未定義動作 bar(*x); // bar(??) } ... int i = 3; foo(cast(invariant)&i, &i); |
invariant はナシ |
文字列リテラルの型 | invariant(char)[] | const char* |
文字列リテラルから非const型への暗黙変換 | なし | あり、ただし非推奨 |