(以下は正式な言語仕様の決定ではなく、単なる私的なメモです)
dmd2.061現在、オブジェクトのコピー時にpostblitがどのように動くかは実はかなり問題がある。
たとえば次の様な構造体を考えよう。
struct ValueArray { int[] arr; }
これはint配列の参照を保持しているだけなので、次のようにコピーのソースとターゲットで同じarrを共有してしまう。
void main() {
auto va1 = ValueArray([1,2,3]);
assert(va1.arr[0] == 1);
auto va2 = va1;
va1.arr[0] = 100;
assert(va2.arr[0] == 1); // 失敗する!
}
ValueArray
は値セマンティクスを持ったオブジェクトとして定義したいので、コピーするときに常にフィールドのarr
もコピーしたい。よってpostblitを次のように定義する
struct ValueArray {
int[] arr;
this(this) { arr = arr.dup; }
}
今度は上のテストが正しく成功するようになる。ここまでは普通のDユーザーなら理解できることだろう。
しかし、上記のコードは実は完全ではない。たとえばint値はmutableからimmutableへコピーによって型変換できるが、ValueArrayはどうだろう。
auto va1 = ValueArray([1,2,3]);
assert(va1.arr[0] == 1);
immutable va2 = va1; // postblitが動いてほしい
が、ここでthis(this)を呼ぶことはできない。なぜならthis(this)はコピー先がコピー元と間接参照を共有しないことを「保証しない」からだ。たとえばValueArrayのpostblitが次の様な定義を持っていたら?
this(this) { arr = arr; } //コピーしてない!
mutableからimmutableにコピーしたのに、実際にはarr
が共有されている状態を作れてしまう。しかも元のva1
から要素を書き換えるとimmutableなはずの値が書き換わってしまう。型システムの保証を破ってしまうのだ。
この場合はどうすればいいのだろう?
ここでちょっと脱線。
「そんなのthis(this)の定義を見れば判るのでは」と思われるが、Dはコンパイル型の言語であり、分割コンパイルもサポートしている。場合によっては関数(postblitも特殊な関数の1つだ)の本体は、別のオブジェクトファイルにあり、あなたはライブラリのdiファイルのみをimportしているかもしれない。
以下の様な構造体の「宣言」はDでは許されている。struct ValueArray { int[] arr; this(this); // 宣言のみ }
つまり、関数本体を調べる方法はコンパイラには取れないのだ。
ここからが本題。結論から言うと、postbllitのセマンティクスは未だ完全には定義されていない。
Andreiが書いたThe D Programming Languageにはこのようなコーナーケースについての記述はないし、その後AndreiがDIP10を書いているがこちらも肝心なところがない(「Copy construction of qualified objects」の節がそれだが内容が空っぽ)。
以前dmd-internalsのNewsgroupでこの事についてAndreiに質問したときのスレッドでは、以下の様な回答を返してくれた。
I agree there's a big problem here.
The worst of it is that postblit ctors make it VERY difficult to typecheck copy construction of qualified objects (const and immutable).
It's much easier to check that some fields get initialized properly one by one, than to make sure the postblit "adjustments" leave the object in the correct state.
We didn't predict this when we designed postblit. It's probably D's largest design mistake.
Going forward I think we should design good copy constructors and migrate away from postblits, while still allowing them (they're very good when they're good - simple adjustments for non-qualified structs).
私は大きな問題があることに同意します。
最悪なのは、postblitコンストラクタが (constやimmutableで)修飾されたオブジェクトのコピー構築について型チェックを行うことが非常に難しいということです。
いくつかのフィールドが1つ1つ適切に初期化されていることをチェックするのは、postblitがオブジェクトを正しい状態に「調整する」ことを確認するよりもはるかに簡単です。
我々はpostblitを設計するとき、我々はこれを予測しなかった。それはおそらくDの最大の設計ミスです。
私は、現状のpostblit(それは修飾なしのstructをコピーするときに単にフィールドを調整する時など、適合するケースではうまく動きます)を引き続き保持しながらも、今後良いコピーコンストラクタを設計し、postblitから移行すべきだと思います。
私はここでAndreiが言っている「postblitがオブジェクトを正しい状態に「調整する」ことを確認する」が、要するにオブジェクトが持つ間接参照のことだと読み取った。コピー先の型修飾子が異なる場合、特にimmutableとmutableで修飾を切り替えるようなコピーではコピー元とコピー先で間接参照先を「共有してはならない」。これをチェックするのは難しい、と。
その対策としてAndreiは、C++のような「コピーコンストラクタ」をDに導入することを考えているようだった。しかし私はpostblitのデザインがどうしても捨てきれず、上のような保証が可能になる方法を模索し続けていた。
その打開策のキーとなるのがpurityから導き出される特性だ。これを使えば
- 共有状態を排除する必要があるとき、コピー先のフィールドはisolatedな値で再代入されることをコンパイラが強制する。
- そのようなpostblitはpureであることを強制する
ことで、コピー先(postblitを呼んだ結果)がコピー元と共有状態を持たないことを型チェックで強制できるはずだ。
具体的にはpsotblitを最低限以下の4種類に分類する。
- コピー元は変更可能な間接参照を持ち、コピー先はコピー元とこれを共有しうる
- コピー元は変更可能な間接参照を持ち、コピー先はコピー元とこれを共有しない
- コピー元は変更不可能な間接参照を持ち、コピー先はコピー元とこれを共有しうる
- コピー元は変更不可能な間接参照を持ち、コピー先はコピー元とこれを共有しない
ここには以下の2つの属性の組み合わせがある
a. コピー元は変更可能かどうか
b. コピー先はコピー元と間接参照を共有しうるかどうか
1と2はコピー元がmutableかconstな場合を想定している。constな値から辿れる間接参照はconstになるが、これはコピー元のオブジェクト(のメソッド)からは変更できなくても、コピー先のオブジェクト(のメソッド)からは変更してもいい、そうしてもconstの定義を破ったりしない、という事に気づけば、const/muableなコピー元を同じものとしてみなせることに納得できると思う。
2のようなケースは最初に例として出したValueArrayの様なケース、1は参照カウンタを共有するRefCountedなケースだ。
3と4はコピー元がimmutableであることを想定している。3はimmutableと間接参照を共有できるので、必然的にコピー先もimmutableであることが暗黙に条件に入っている。4はコピー元がimmutableでも、全てのフィールドがisolatedな値で再代入されるので、postblit呼出し直後はコピー先の値全体がisolatedな値であることが保証できる。つまり、結果をmutableやconstな値として扱っても問題は生じない。
さて、お待ちかねのsyntaxだ(みなさん好きでしょう?)。
a.の特性はpostblit関数自体のqualifierで指定する。b.のためには、this(pure this)
という書き方を新たに許すことにする。そうすると、上の4種類の分類は次のように書ける。
-
this(this);
orthis(this) const;
-
this(pure this);
orthis(pure this) const;
this(this) immutable;
this(pure this) immutable;
1,2でconstが付く場合は、「このpostblitはコピー元から見える間接参照を共有[します|しない]が、それを変更はしません」という意味になる。
対応して、3,4でimmutableが付くのは、「コピー[元|先]の間接参照は不変値ですよ」と表明する意味になる。
this(pure this)
というsyntaxを導入する意図は、たとえば1の分類のpostblitを書き、しかしその処理をpureにしたい場合に役に立つ。
this(this) pure;
こう書くと、pureはpostblitの処理本体にのみ影響し、コピーのセマンティクス自体には影響しない。(紛らわしいと思うが、キーワードの再利用と既存概念との組み合わせを優先したらこうなった。より良いsyntaxがあればぜひともお教え願いたい)
this(pure this)
と書いた場合は、暗黙にpostblit処理本体もpureであることが強制される。つまりthis(pure this) pure
と書いても意味は同じになる。前者は後者の構文糖として扱う。
これによって、以下の様な変換マトリクスが定義できる(見苦しい表ですまない)
+---------------------------------------------------------------------------------------------------+
|indirection | target |
|source and |---------------------------+---------------------------+---------------------------|
|target |mutable |const |immutable |
|---------------+---------------------------+---------------------------+---------------------------|
|source |m |this(this) |this(this) | |
| | |this(pure this) |this(pure this) |this(pure this) |
|---------------+---------------------------+---------------------------+---------------------------|
| |c |this(this) |this(this) | |
| | |this(pure this) |this(pure this) |this(pure this) |
|---------------+---------------------------+---------------------------+---------------------------|
| |i | | |this(this) immutable |
| | |this(pure this) immutable |this(pure this) immutable |this(pure this) immutable |
+---------------------------------------------------------------------------------------------------+
厳密にはこれはラフな変換表で、実際にはpostblitを複数書いた場合にある変換でどれが優先されるか、などのコーナーケースの定義が必要だろう。(this(pure this)版は一般的にコストが大きいと考えられるので、同程度のthis(this)があるならそれを優先する、など)
以上がpostblitの現時点での問題と、その改善方法の私案だ。
私はこれをDIPとして提案する予定だが、英語の文章を書く方がこれをコンパイラに実装するよりよっぽど大変だというのは皮肉ではない。
もちろん、何か見落としがある可能性があるので、質問等は歓迎する。