言語設計の理由
Dの言語設計に関する多くの決定の理由について、 頻繁に質問されることがあります。ここに、その大部分について説明します。
演算子オーバーロード
なぜ operator+(), operator*() のような名前にしなかったのですか?
C++はまさにその方式で、'+' をオーバーロードしたいときに 'operator+' という名前を使えるのはなかなか魅力的でもあります。 問題は、物事はそううまくはいかないということです。たとえば、 比較演算子は <, <=, >, >= の4つがあります。C++では、 完全を期すならば4つの演算子全てをオーバーロードしなければなりません。Dでは、 opCmp() 関数だけを定義すれば、 全ての比較操作は意味解析によって導出されます。
operator/() のオーバーロードを考えてみても、メンバ関数として、 対称性を保って逆演算をオーバーロードする方法がありません。たとえば、
class A { int operator/(int i); // (a/i)をオーバーロード static operator/(int i, A a) // (i/a)をオーバーロード }
二個目の定義が逆演算のオーバーロードになっていますが、 この定義はvirtualにできませんし、 一個目の定義と対称的でないので混乱のもとです。
なぜ、グローバル関数として演算子オーバーロードをできるようにしていないのですか?
- 演算子オーバーロードは、オブジェクトが引数の時にのみ行えます。
ですから、論理的に言ってそのオブジェクトのメンバ関数に属するわけです。
この場合、二つのオペランドが違う型のオブジェクトだった場合どうすべきか、
という疑問が残ります:
class A { } class B { } int opAdd(class A, class B);
opAdd() は class A と B のどちらにあるべきでしょうか? 第一引数のクラスというスタイルが、明らかな解決策です。class A { int opAdd(class B) { } }
- 演算子オーバーロードの際には、クラスの private メンバへのアクセスが必要となることがよくあり、グローバルにしてしまうと オブジェクト指向的なクラスのカプセル化の理念に反します。
- (2) は、演算子オーバーロードに関しては自動的に "friend" アクセス権を 得ることにすれば解決されます。しかし、そのような例外的な動作は、 Dをシンプルに保つという考えにそぐいません。
なぜユーザー側で新しい演算子を定義できるようになっていないのですか?
さまざまなUnicodeの記号を中置演算子として使えるようにすると、 確かに役に立つこともあるでしょう。問題は、D ではトークンは 意味解析とは完全に独立であるとされていることです。 ユーザー定義の演算子はこの仮定に反します。
なぜユーザー側で演算子の優先順位を定義できるようになっていないのですか?
問題は、これによって構文解析が影響を受けてしまうことです。 そしてDでは、構文解析は 意味解析とは完全に独立であることになっています。
なぜ __add__ や __div__ といった名前を opAdd, opDiv の代わりに使わなかったのですか?
__ を使った予約語は、処理系特有の言語拡張を表すべきで、 言語の基本的な部分に使うものではないと考えています。
なぜ、二項演算子のオーバーロードを static メンバにして両方の引数を示せるようにして、 逆演算の定義の問題をなくそうとしなかったのですか?
こうすると演算子オーバーロードをvirtualにすることができなくなってしまい、 static 関数は実際の処理をする virtual 関数へと処理を渡すだけの ラッパとして実装されることになりがちです。これは醜いhackのようになります。 第二の理由として、opCmp() 関数が既に Object クラス内に演算子オーバーロードとして 存在し、いくつかの理由から、virtualである必要があります。 この関数だけを他の演算子オーバーロードの形と非対称にしておくのは、 不要な混乱のもとになります。
プロパティ
なぜ D には、浮動小数点型の無限大を得る T.infinity のようなプロパティが、 言語のコア機能として存在するのですか? C++ の std::numeric_limits<T>::infinity のようなライブラリによる実装も可能なはずです:
言い換えると、「既存の言語で表現できるものを、なぜ言語のコア機能として組み込むのか?」 という質問になります。 T.infinity の場合で考えてみましょう:- コアの言語機能として組み込むと言うことは、コアの言語が浮動小数点数の 無限大について知っていることを意味します。 単に定数のビットパターンの上に定義されている場合、コア言語の処理系には 無限大について知識を持てず、誤って使用された時にわかりやすいエラーメッセージを表示できなくなります。
- (1) の副作用として、 定数畳み込みを始めとした様々な最適化を用いるのが難しくなります。
- テンプレートのインスタンス化や#includeによるファイルのロードなどは、 どれもコンパイル時間とメモリを消費します。
- しかし、もっとも悪いのは、単に無限大を取得するために長々とコードを書かねばならないというのは、 「言語とコンパイラは IEEE 754 について何も知らず、 それに頼ることができない。」と暗示しているということです。 そして実際に、 その他の点では優秀なC++コンパイラでも、 浮動小数点数の比較においてNaNを正しく扱わないものが数多く存在します。 (Digital Mars C++ は正しく比較を行います) C++98 規格は式やライブラリ関数におけるNaNと無限大の扱いについて 何も言及していませんから、これは動作しないと仮定してコードを書かねばなりません。
まとめると、単なるビットパターンを返すこと以外にも、 NaN や 無限大をサポートするには沢山の作業が必要なのです。それは コンパイラの主要なロジックに組み込まれねばならず、浮動小数点数を扱う 全てのライブラリコードに浸透しなければなりません。そして、言語標準に含まれている必要があります。
わかりやすく書くと、op1 か op2 が NaN ならば、その時:
(op1 < op2)
は、NaNが正しく処理されていれば:
!(op1 >= op2)
と異なる結果になります。
なぜ static if(0) が if (0) の他に存在するのですか?
if(0) にはいくつか制限があります:
- if (0) は新しいスコープを導入しますが、static if(...) は違います。
これはどういうことでしょう? 条件的に新しい変数を宣言したいときに、この違いが問題になります:
static if (...) int x; else long x; x = 3;
これに対して:if (...) int x; else long x; x = 3; // エラー。x は未定義
- static if で偽となる条件については、意味的に正しく動作するコードである必要がありません。
たとえば、
他の箇所で条件的に宣言された識別子に依存してもかまいません:
static if (...) int x; int test() { static if (...) return x; else return 0; }
- static if は宣言のみを置ける箇所ででも使用できます:
class Foo { static if (...) int x; }
- static if は型の新しい別名を宣言できます:
static if (0 || is(int T)) T x;