本日、DIP49をVersion 2に更新し、DIP53を新しく投稿しました
http://wiki.dlang.org/DIP49
http://wiki.dlang.org/DIP53
せっかくなのでこれらの概要を日本語で書いておきたいと思います。
DIP49 - Define qualified postblit
D言語に於いて、構造体のコピーはメモリコピーとpostblitの実行の組み合わせで表現されます。
- メモリのコピー
コピー元となるオブジェクトのビットイメージをコピー先に単純に複写する。 - postblitの実行
コピーされたビットイメージに対して後処理を行い、コピー後のオブジェクトを調整する
このpostblitというコンセプトは「基本は単純、必要なら複雑なことをする」というシンプルなもので、C++のコピーコンストラクタと異なりコピー元を明示的に触る必要がない点でも優れています。
しかし現在の言語仕様ではオブジェクトが間接参照を持つ場合のコピーについて言及がないため、postblitでは問題が起きうるケースでのコピーが実現できない、という制限があります。(起きうる問題についてはここを参照)
これを解決するため、DIP49では以下の4種類のpostblitを定義し、この問題に対処します。
Mutable Postblit
this(this) { ... }
の様に定義します。
mutableなオブジェクトからmutableなコピーを作るために使われます。
オブジェクトが保持するmutableな間接参照を、コピー元と共有したままにもできますし、新しいデータを参照するよう変更することもできます。
オブジェクトが保持するmutableな間接参照をpostblit実行中に操作できるので、コピー操作がコピー元の表現を変更する必要がある場合、これを使います(例:RefCounted
)
Immuatable Postblit
this(this) immutable { ... }
の様に定義します。
immutableなオブジェクトからimmutableなコピーを作るために使われます。
間接参照部分はコピー元と共有したままにも、新しく割り当てることもできます。
間接参照部分は必ずimmutableとして型修飾されるので、間接参照先を変更することはできません。
Inout Postblit
this(this) inout { ... }
の様に定義します。
ある型のオブジェクトから同じ型のコピーを作るために使われます(mutable to mutable, const to const, immutable to immutable, etc)。
間接参照部分はコピー元と共有したままにできます、その部分はコピー元とコピー先が同じ型になることが保証されるので問題は起きません。
間接参照部分を変更することはできません。これはたとえこのpostblitがmutable to mutableのコピーのために呼び出された場合でも、です(そもそもコピーされるオブジェクトの型をpostblitの中で知ることはできない)。
間接参照部分は新しく割り当てることもできます。
この場合、割り当てる新しいデータは Unique Expression という定義を満たす必要があります。
Unique Postblit (Const Postblit)
this(this) const { ... }
の様に定義します。
任意の型修飾子を持つオブジェクトから任意の型修飾子を持つコピーを作るために使われます(mutable to immutable, const to immutable, etc)。
間接参照部分は必ず新しく割り当て直す必要があります(コピー元と共有したままにはできません)。
このとき、割り当てる新しいデータは Unique Expression という定義を満たす必要があります。
DIP53 - Qualified constructor revisited
2.063からqualified constructorが導入されたのですが、この実装された機能の定義が判りにくい&制限がきついものだったため、DIP53では型修飾されたオブジェクトを構築するためのconstructorの定義を改良する提案を行っています。
Mutable Constructor
this(...) { ... }
の様に定義します。
mutableなオブジェクトを構築するために使われます。
Imutable Constructor
this(...) immutable { ... }
の様に定義します。
immutableなオブジェクトを構築するために使われます。
Inout Constructor
this(inout ...) inout { ... }
の様に定義します。
一つ以上のinoutな仮引数を持つ必要があります。
これは引数に与えたデータと同じ型修飾子を持つオブジェクトを構築したい、という場合に使われます。
たとえばmutableな配列を取ったときはmutableなオブジェクトを、immutableな配列を取ったときはimmutableなオブジェクトを作りたい、という場合にコンストラクタの定義を1つに纏めることができます。
strcut Array(E) {
E[] data;
// コンストラクタ定義が1つだけ
this(inout(E)[] arr) inout { this.data = arr; }
}
auto makeArray(E)(inout(E)[] data) { // ヘルパー関数
return inout Array!E(data);
}
void main() {
int[] marr = [1,2,3];
immutable int[] iarr = [4,5,6];
auto ma = makeArray(marr);
assert(ma.data == [1,2,3]);
ma.data[0] = 10;
assert(marr == [10,2,3]);
// 元データmarrを共有している
auto ia = makeArray(iarr);
assert(ia.data == [4,5,6]);
static assert(!is(typeof(ia.data[0] = 10)));
// 元データiarrを共有しつつ書き換え不可能
}
Unique Constructor (Const Constructor)
this(...) const { ... }
の様に定義します。
任意の型修飾子を持つオブジェクトを構築するために使われます。引数に制限はありません。
コンストラクタにはconstが付いていますが、間接参照を持つfieldを初期化する際、割り当てる新しいデータは Unique Expression という定義を満たす必要があります。
struct UniqueArray(E) {
E[] data;
this(inout(E)[] arr) const {
//this.data = arr; // NG, arrはUnique Expressionの定義を満たさない
this.data = arr.dup; // OK, dupはint[]の配列からUnique Expressionを作ることができる
}
}
void main() {
int[] marr = [1,2,3];
// どんな型修飾子のUniqueArray!intでも1つのコンストラクタで構築できる
auto ua1 = UniqueArray!int(marr);
auto ua2 = immutable UniqueArray!int(marr);
// 構築されたUniqueArray!intオブジェクトは、
// 外部の変更されうるデータへの参照を持たないことが保証される
assert(ua1.ptr !is marr.ptr);
assert(ua2.ptr !is marr.ptr);
}
この2つのDIPの根底には以下の様なコンセプトが存在します。
-
本質的に、オブジェクトにはmutableかimmutableかの2種類しかない。それぞれを構築/コピーするために
- Mutable Constructor/Postblit
- Immutable Constructor/Postblit
のそれぞれ2種類が最低必要になる。
-
Dにはmutableかimmutableかという状態を隠して同じように取り扱うためのconst, inoutという型修飾子がある。
また、コピーでは「mutableなオブジェクトからimmutableなコピーを作る」という操作も必要になる。
このため、コンストラクタの引数/コピー元が持つ間接参照を、構築された/コピー先のオブジェクトが- 共有可能なInout Constructor/Postblit
- 共有しないことを保証するUnique Constructor/Postblit
のそれぞれ2種類をさらにサポートする。
Inout Constructor/Postblitでは間接参照を共有可能なことから、この部分の型修飾を変えてしまわないよう
「結果として同じ(または弱い)型修飾のオブジェクトしか得られない」という制限が付く。Unique Constructor/Postblitでは、
「ある新しく作られたオブジェクトが持つ間接参照が全てUniqueである(オブジェクトの外にあるデータを参照しないこと)を保証できるならオブジェクトそのものもUniqueになることが保証でき、つまりそのオブジェクトをどんな型修飾子で取り扱っても型システムの保証の破れは起きえない」
という考えに基づいている。
ちなみに現状実装されているUnique Constructorはこの保証をpure属性を使って行っているのですが、そのために使いづらい/わかりにくい定義になってしまっている。
PostblitとConstructorで同じコンセプトに基づいていることから、Syntaxも意図して同じように見えるように定義してあります (DIP53と合わせるために、DIP49はVersion 1→2でSyntaxの記法を変えました。興味がある方はwikiの編集履歴をどうぞ)。
Unique Expression (Unique式)とは、上記にある「Uniqueなデータ」というコンセプトを他の組み込みリテラルなどに拡張したもので、現時点では以下の様な定義を行っています。
-
基本リテラル(整数、文字)
-
複合リテラル(構造体リテラル、配列リテラル、連想配列リテラル)
- リテラルが部分式を持つ場合、それらは全てUniqueである必要がある
-
間接参照を持たない式
たとえば、整数の乗算は右辺値を返し、また整数は間接参照を持ちません。従ってUniqueになります。int a, b, c = a * b; // a * bはUnique式になる
-
Unique Constructorによって構築されたオブジェクト
-
Unique Postblitによってコピーされたオブジェクト
-
Uniqueオブジェクトのフィールド
unique_obj.var
はUnique式となる -
Unique式のアドレス
&unique_obj
はUnique式となる -
配列のコピー
もし配列要素の型がUnique Copyをサポートしている場合
unique_array.dup
unique_array.idup
-
Uniqueな配列の要素
unique_array[n]
unique_array[n .. m]
-
配列の連結
定義から、連結式は常に新しく確保された配列を返します。従って、もし配列要素の型が間接参照を持たない場合、
連結の結果はUniqueになります。 -
Uniqueなオブジェクトを返すpure関数の呼び出し
-
New式
構造体がnewされる場合は、「複合リテラル」のケースと同等です
クラスがnewされる場合は、Unique Constructorによるオブジェクト構築のケースと同等です
その他の間接参照を持たないオブジェクトが構築される場合、結果はUnique式として扱われます
こんなところでしょうか。
原文ではPostblitがオーバーロードされている場合の挙動や、druntimeにどのような変更が必要なのか、なども書いてあります。