const(FAQ)
D の const/immutable システムは独特なものなので、 多くの質問があると思われます。
- 何故 D には const があるのか?
- D の const の設計を決める原理は何か?
- 推移的 const とは何か?
- 頭部 const とは何か?
- 尾部 const とは何か?
- 論理的 const とは何か?
- 読み取り専用ビューの意味で readonly を使わないのは何故か?
- 何故 Java は const を排除したのか?
- C++ の const とどう違うのか?
- なぜ文字列がimmutableなのか?
- 関数の引数がデフォルトで const でないのは何故か?
- クラスの静的メンバは推移的 const でカバーされるのか?
- immutable は何が嬉しいのか?
何故 D には const があるのか?
D 2.0 の const と immutable に対するフラストレーションが表明されることは多く、 どんな価値があるのかという疑問もよく耳にします。
- 関数のインターフェイスがより自己説明的 (self-documenting) になります。 推移的 const なしでは、ポインタ/参照に関する const 性は、 ドキュメントに頼るしかなくなってしまいます (そしてドキュメントというのはいつも抜けがあったり古かったり間違っていたりするものです) 推移性のない C++ の const は、この目的ではほとんど意味のない物体であり、 それが C++ のプログラマが慣習によってこの問題を解決しがちな理由です。
- インターフェイスを信頼できるものにします。 これは多くの人が同じコードに関わるようになればなるほど重要さが増してきます。 言葉を換えると、スケーラビリティが高いのです。 巨大なチームからなるプロジェクトに関わる人々は、 const なしではコンパイラに規約を強制させる術がないため、 物事がずっと大変になってしまうと言っています。チームが大きくなればなるほど自体は悪化します。 API の管理は巨大なプロジェクトではクリティカルな問題です - これが (極端な例でいえば) BASIC がスケールしない理由です。
- const の推移性は、いくつか興味深い最適化の機会を提供します。 この事実の価値はまだまだこれから考察や活用の余地があります。
- そしてこれが最大のポイントです。ポイント 1 ~ 3 はこれに比較すると些細なものです。 これからのプログラミングは、マルチコア、マルチスレッドになります。 そのようなプログラミングを容易にする言語こそが求められるものとなるでしょう。 推移的 const は D をこのパラダイムへと移行させる鍵となります。 Haskell や Erlang の流行が、このようなトレンドが来ることの証拠とも言えます (これらの言語の killer feature は、マルチプログラミングの容易さです)。 C++ は、簡単な方法でマルチプログラミングをサポートするように適応するのは難しいでしょう。 D もまだその段階ではありませんが、その方向へ向かっています。そして、 推移的 const がそのための絶対的な基礎となる技術です。
もちろん、シングルスレッドのそんなに大きくないプログラムを一人で書く場合であれば、 const はそこまで有用ではないでしょう。 D の const は単にそれを使わないことで、あるいは D 1.0 を使うことで、 無視することができます。const が強制される唯一の箇所は、 書き換え不可の文字列型 string のみです。
D の const の設計を決める原理は何か?
- 数学的に健全であること。つまり、 合法的に const を逃げる術がないこと。
- すべての型を構造体でラップでき、ラップした構造体が 依然として同じ const に関する挙動を示すこと - 言い方を変えると、特定の型に対するマジカルな規則などがないこと。
- const の挙動が推移的であること。
- const の挙動がすべての型 T に関して同じであること。
推移的 const とは何か?
推移的 const とは、型に const が適用されたら、 再帰的のその型の全ての部分にまで const が適用されることをいいます。したがって、次の例のようになります。
const(int*)** p;
p += 1; // ok, p は mutable
*p += 1; // ok, *p は mutable
**p += 1; // エラー, **p は const
***p += 1; // エラー, ***p は const
推移性があると、 mutable int への const ポインタ を定義することは不可能になります。
C++ の const は推移的ではありません。
頭部 const とは何か?
頭部 const は、const がそれに隣接した型にだけ適用されるタイプの const です。例として
headconst(int**) p;
は、p は mutable int への mutable なポインタへの const なポインタ という意味になります。D には頭部constはありません (上の headconst は単なる例示です) が、C++ の const は頭部 const システムです。
尾部 const とは何か?
尾部 const は頭部 const をちょうど反転したものです - const 型から到達できるものは、トップレベルだけを除いて全て const になります。 例として:
tailconst(int**) p;
は、p は const int への const ポインタへの mutable ポインタ という意味になります。頭部 const と尾部 const を両方合わせると、 推移的 const になります。 D には tailconst という特別な型コンストラクタはありません (上の例は単なる例示です)。
論理的 const とは何か?
論理的 const とは、外側からは状態が変わらないように見えるが、 実際は物理的には const でないデータのことを言います。 例として、遅延評価をするオブジェクトがあります:
struct Foo {
mutable int len;
mutable bool len_done;
const char* str;
int length()
{ if (!len_done)
{ len = strlen(str);
len_done = true;
}
return len;
}
this(char* str) { this.str = str; }
}
const Foo f = Foo("hello");
bar(f.length);
この例では、f.len の計算は必要なときだけ行われます。 Foo は、外側からの観察者にとっては オブジェクトから得られる値は構築後変化しないので、論理的const です。 mutable という修飾子は、 たとえ Foo のインスタンスが const であっても、そのフィールドが依然として変更可能なことを示します。 C++ では論理的 const の概念がサポートされていますが、D では違います。 また、D には mutable 修飾子もありません。
論理的 const の問題点は、const が推移的でなくなることです。 推移的でないということは、スレッドによるレース条件が潜在し、 opaque な const 型が mutable なメンバを持つかそうでないか判別する術がなくなるということです。
参考文献: mutable: bitwise vs. logical const
読み取り専用ビューの意味で readonly を使わないのは何故か?
readonlyはソフトウェアではすでに ROM / Read Only Memory / 書き換え不可メモリという意味が確立されています。 ハードウェアによるメモリ保護機能のあるコンピュータでは、 readonly にはメモリの内容が置き換わらないという意味もあります。 D で readonly をメモリの読み取り専用ビューの意味で使うことは、 別の参照やthread経由でデータが変更されうることを考えると、慣習と反しています。
何故 Java は const を排除したのか?
http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4211070
C++ の const とどう違うのか?
C++ には他の全ての言語の中でもっとも D に近い const システムがありますが、 やはり大きな違いがあります:
- const が推移的でない
- immutable がない
- const オブジェクトに mutable メンバを持たせられる
- const を合法的にキャスト外ししてデータを変更可能
- const T と T が必ずしも別の型になるとは限らない
なぜ文字列がimmutableなのか?
関数の引数がデフォルトで const でないのは何故か?
ほとんど (ほぼ全て?) の関数の引数は変更されませんから、 デフォルトを const にして、 変更したいときにだけ特別な記述をする方が理にかなっているようにも思えます。 この方法の欠点は:
- 過去の D の習慣、そして C, C++, Java, C#, などでの習慣を大きく乱すことになります。
- 新しい予約語、mutable のようなものが必要になってしまいます。
- 一番まずいのが、宣言に一貫性がなくなることです:
void foo(int* p) { int* q; ... }
p は const を指しますが、q は mutable を指すことになります。 この手の一貫性のなさはあらゆる種類のミスを呼びます。 また、 型を扱うジェネリックなコードを書くのが非常に難しくなってしまいます。
in を使用することで、 const で修飾する醜さをやわらげることは可能です:
void str_replace(in char[] haystack, in char[] needle);
クラスの静的メンバは推移的 const でカバーされるのか?
クラスの静的メンバはプログラムのグローバル状態の一部であり、 オブジェクトの状態の一部ではありません。従って、 mutable な静的メンバを持つクラスが そのクラスのオブジェクトの推移的 const 性を違反するということはありません。
immutable は何が嬉しいのか?
immutable なデータは、一度初期化されたら二度と変化しません。 この性質には様々な利用法があります:
- immutable データへのアクセスは、 複数のスレッドによって読み取りが行われる場合でも同期は不要です
- Data race、tearing、順序一貫性 (sequential consistency)、 キャッシュ一貫性 (cache consistency) などは全て、 immutable なデータを扱うときには問題になりません。
- Pure 関数が引数にとることができるのは immutable データのみです。
- データ構造の deep copy を行うとき、 immutable な部分はコピーの必要がありません。
- immutable 性によって、 実際は参照で渡されるデータを値であるかのように扱うことが可能になります。 (文字列が、このケースに当てはまるよく使われる例です)
- immutable 型は、プログラマに、より self-documenting な情報を提供します。
- immutable データは、ハードウェア的に保護された読み取り専用メモリや、 ROMに配置することすら可能です。
- immutable データの中身が変わるとしたら、 それは確実にメモリ破壊のバグなので、 そのようなデータ一貫性を自動的にチェックすることが可能です。
- immutable 型によって、 さまざまな最適化の可能性がもたらされます。
const は mutable な世界と immutable な世界の橋渡しとなります。 const で引数を受け取ることで、一つの関数で mutable なデータと immutable なデータの双方を扱うことができます。