さて、本日はコンパイラ現在話ということで、今のDで使える機能のちょっとわかりにくい所について説明したいと思います。(以下の内容は2012年末までにはrelease予定の2.061をベースにしています)
====
if statement上でのopCastの動き
import std.stdio;
void main() {
struct S {
bool opCast(T)() if (is(T == bool))
{ return true; }
}
S s;
if (auto x = s) {
// ここでxの型は何になる?
writeln(typeof(x).stringof);
}
}
if文は演算子オーバーロードでs.opCast!boolを解決し、その結果が真ならthen節を実行しますが、この時条件式の結果を変数に束縛する場合、xの型はboolではなくSになります。
is式によってテンプレート型のマッチングを行う
2.061ではis式によるマッチングがさらに改善され、タプルによってテンプレート型を実体化したときのパラメータを取得できるようになっています。
import std.typecons;
void main()
{
Tuple!(int, string) t1;
Tuple!(string, int[]) t2;
foo(t1);
bar(t2);
}
void foo(T)(T t) {
static if (is(T unused == Tuple!Args, Args...)) {
// Tがstd.typecons.tupleのインスタンスだった場合のみここに来る
// Argsには(int, string)が入っている
}
else
static assert(0);
}
void bar(T)(T t) {
static if (is(T unused == X!Args, alias X, Args...)) {
// TがX!Argsで実体化された型の場合のみここに来る
// Xにはstd.typecons.Tupleテンプレートが、Argsには(string, int[])が入っている
static assert(__traits(isSame, X, std.typecons.Tuple)); // テンプレートシンボルが同一かどうかの確認
static assert(is(Args[0] == string));
static assert(is(Args[1] == int[]));
}
else
static assert(0);
}
これに関連して、unused
のような不要な宣言を省けるような機能拡張を提案中です。
Issue 8959 - IsExpression should support syntax which has no Identifier in all cases
__traits(compiles)とis(typeof())の違い
__traits(compiles)
はその場でコードの意味解析を行い、エラーが発生したらfalseを返します。is(typeof())
はこれに対して以下のいくつかの意味解析上のチェックが行われない点が異なります。
-
this, super変数の有効性チェック
this
,super
はメンバ関数外では通常使えませんが、typeof
内部では使えるようこのチェックの抑制が働きますstruct S { S* x = &this; // Error alias X = typeof(this); // OK }
-
コンストラクタ内での
super()
やthis()
の呼び出しフロー解析
以下の様なコードがフロー解析に引っかからないようにするためです。class B { this(string s) {} } class C(T) : T { this(string s) { static if (is(typeof(super(s)))) { } super(s); // 最初の基底クラスのコンストラクタ呼出しになる super(s); // Error: multiple constructor calls } } alias X = C!B;
-
@safe/pure/nothrowチェック
void not_s() nothrow pure {} void not_p() nothrow @safe {} void not_n() @safe pure {} void main() @safe pure nothrow { //not_s(); // Error //not_p(); // Error //not_n(); // Error pragma(msg, is(typeof( not_s() )), ", ", __traits(compiles, not_s() )); // true, false pragma(msg, is(typeof( not_p() )), ", ", __traits(compiles, not_p() )); // true, false pragma(msg, is(typeof( not_n() )), ", ", __traits(compiles, not_n() )); // true, true [*] }
[*]…nothrowチェックって__traits(compiles)でも抑制されるのか? 今度調べてみます。
Nested structの厳密な定義
nested structとは外側スコープの変数にアクセス可能な構造体定義のこと。
void main() {
int x = 1;
struct SA {
int n;
void foo() { x = n; }
}
SA sa = SA(2);
assert(x == 1);
s.foo();
assert(x == 2); // mainのローカル変数xが書き換わった
static assert(SA.tupleof.length == 2); // SAはメンバ変数n以外に隠れたフレームポインタを持つ
static assert(SA.sizeof == int.sizeof + (void*).sizeof);
// 隠れたフレームポインタの分だけ構造体のサイズが増えている
一般的には関数内に定義されている構造体をnested structと呼んでいますが、厳密には上記の
隠れたフレームポインタを持つものだけが(D言語仕様的な)nested structと言えます。
また、外側の変数にアクセスするコードがある=メンバ関数を持つことなので、
メンバ関数を持たない構造体の場合は隠れたフレームポインタが付加されません。
void main() {
struct SB {
int n;
}
static assert(SB.tupleof.length == 1); // SBは隠れたフレームポインタを持たない
static assert(SB.sizeof == int.sizeof);
}
また、今の所nested structになれるのは
- 関数内のローカル変数にアクセスするstruct
の1パターンのみです。struct/classの中に定義されたstructはnestedにはなりません。
(2012/12/17訂正: クラス内のstructはnested structになりません。class内class(nested class)と混同してしまっていたようです。お詫びして訂正致します)
struct S {
int n;
struct N { int v; void g(){} }
}
pragma(msg, S.sizeof); // prints 4
pragma(msg, S.N.sizeof); // prints 4, context pointerのサイズを含まない
これについてはTDPLに記述があるらしいのですが、はっきりとした理由は調べ切れてません。
Eponymous templateの応用例
Dでは、あるテンプレートが同名のシンボルをメンバに持っている場合、メンバの部分を省略できます(というか省略を強制される)
// T.initを取り出すテンプレート
template GetInit(T) {
enum GetInit = T.init;
}
//static assert(GetInit!(int).GetInit == int.init); // こうではなく
static assert(GetInit!(int) == int.init); // こう書ける
昔はこの機能を使うとき同名のシンボル以外のものを何も書けませんでしたが、現在ではこの制限がほぼなくなっています。
template Calc(T) {
import std.traits; // 他のモジュールの機能を使うためにimportを書いても
static if (isIntegral!T) { // コンパイル時の条件分岐を書いても
T calc() { return T.init + 10; } // コンパイル時に実行させるための関数を定義しても
enum Calc = calc(); // 最終的に"Calc"というシンボルが見つかればOK
}
else
static assert(false);
}
enum x = Calc!int;
template func(string name) {
void func() {}
void func(int n) {} // オーバーロードされた関数でもOK
}
void test1() {
func!"name"();
func!"name"(10);
}
void foo() {}
void bar(string s) {}
template call(string name) {
version(none) { //これはまだ駄目だが…
alias call = foo;
static if (name == "yes")
alias call = bar;
} else {
static if (name == "yes") {
alias ov = foo; // ↓
alias ov = bar; // 明示的にオーバーロードセットを作ってから
alias call = ov; // 同名のシンボルとしてaliasを定義するとOK
}
else
alias call = foo;
}
}
void test2() {
call!"no"();
static assert(!__traits(compiles, call!"no"("bad")));
call!"yes"();
call!"yes"("hello");
}
2.061で直ったbug 5893では、これを使って演算子オーバーロード用の関数を非テンプレートな関数に委譲するテクニックが出ています。
class C {
void concatAssign(C other) { }
void concatAssign(int other) { }
template opOpAssign(string s) if (s == "~=")
{ alias concatAssign opOpAssign; }
}
void main() {
c ~= 1; // line 15
}
クラスの仮想関数で、実行時バインディングを回避する方法
これも2.061で直ったbug 8809ですが、あるオブジェクトから実行時のバインディングを回避して基底クラスの関数を呼び出したい場合、obj.BaseClass.func()
構文が使えます。
以下のサンプルコードは http://dlang.org/function から引用
class B {
int foo() { return 1; }
}
class C : B {
override int foo() { return 2; }
void test() {
assert(B.foo() == 1); // this.B.foo()に変換され、
// B.fooを静的に呼び出す
assert(C.foo() == 2); // 実際のthisインスタンスの型がDであっても
// C.fooを静的に呼び出す
}
}
class D : C {
override int foo() { return 3; }
}
void main() {
auto d = new D();
assert(d.foo() == 3); // D.fooを呼び出す
assert(d.B.foo() == 1); // B.fooを呼び出す
assert(d.C.foo() == 2); // C.fooを呼び出す
d.test();
}
構造体のコンストラクタ呼出しの改良
構造体のコンストラクタが引数を1つだけ取る場合、これを暗黙に呼べる機能がDにはあります。
import std.bigint;
void main() {
BigInt n1 = BigInt(1); // コンストラクタ呼出し
BigInt n2 = 1; // 暗黙のコンストラクタ呼出し、BigInt(1)と同じ
}
従来はこの記法は関数スコープのみ、かつ実行時の変数を定義する場合のみしか使えなかったのですが、2.061ではこの機能が正式にどのスコープでも使えるようになりました(bug 7019の修正)
struct S {
int store;
this(int n) { store = n; }
}
enum S global_constant_s = 2; // 可能になった
S global_variable_s = 2; // 可能になった
class C {
enum S field_constant_s = 4; // 可能になった
S field_variable_s = 4; // 可能になった
void foo() {
enum S local_constant_s = 3; // 可能になった
S local_variable_s = 3; // 従来からできた
}
}
さらにはこんな記法も可能にしようと現在作業中です…
BigInt[] intarr = [1, 2, 3];
//BigInt[] intarr = [BigInt(1), BigInt(2), BigInt(3)]; と同じ
メンバ関数の継承とその属性推論
Dにはpure, nothrow, @safeといった有用な属性が組み込みで定義されていますが、基底クラスのメンバ関数がこれらの属性を持っていた場合、派生クラスでこれを継承したらどうなるのでしょうか。
class B {
void foo() pure nothrow @safe {}
}
class D : B {
//override void foo() pure nothrow @safe {} // 省略なし
override void foo() {} // オーバーライドした関数から属性を推論(継承)する
pragma(msg, typeof(D.foo)); // pure nothrow @safe void()
}
この通り、D.fooは自動的にpure
nothrow
@safe
を持つようになっています。これは、派生クラスのメンバ関数はオーバーライドした基底クラスのメンバ関数より弱い保証を持てない、という性質から来る必然的な挙動です。
実例で説明しましょう。もしD.fooがnothrowでなくなることができる(B.fooより弱い保証を持つ)ことができると、B.fooのnothrow性が破られてしまいます。
class B {
void foo() nothrow {}
}
class D : B {
override void foo() { throw Exception("wow!"); }
// 例外を投げる(正しいDのコードではない)
}
void main() {
B b = new D(); // DのインスタンスをBとして保持
b.foo(); // B.fooは例外を投げないハズ…?→実行時エラー!
}
このようなコードは属性による保証をめちゃくちゃにしてしまうため、コンパイラによって静的に検出され、従ってD.fooは必ずnothrowになるわけです。上で見たとおり、これはpureと@safeにも同様に適用されます。
ちなみに、@truestedはどういう扱いになるかというと…
class B {
void foo() @trusted {}
}
class D : B {
override void foo() {} // オーバーライド
pragma(msg, typeof(D.foo)); // @safe void()
}
D.fooは@safe
になりました。これは@trustedな関数は基本的に安全でないため、これをオーバーライドした派生クラスのメンバ関数が、意図せず@trustedにならないよう安全側に倒しているためです。D.fooも@trustedにしたい場合は、そのことを明示的にコンパイラに指示してやる必要があります。
class B {
void foo() @trusted {}
}
class D : B {
override void foo() @trusted {} // オーバーライド
pragma(msg, typeof(D.foo)); // @trusted void()
}
値範囲伝搬(Value Range Propagation)
Dでは算術演算の結果は最低int型にまで昇格されます。このため、以下のコードではa + b
の結果はint
型になり、従ってcへの代入はintからbyteへの狭化変換になるためコンパイルエラーとなります。
byte a, b, c;
c = a + b; // Error: cannot implicitly convert expression
// (cast(int)a + cast(int)b) of type int to byte
これは意図しないオーバーフローを防ぐためCから改良された箇所ですが、プログラマが本当にオーバーフローを無視したい場合、以下の様にキャストが必要になります。
c = cast(byte)(a + b); // OK. キャストで狭化変換を強制
これは意図通りに動作しますが
- 醜いcastがある
- 将来
a
,b
,c
の型がbyteから変わった場合、キャストの型も変える必要がある
などいくつか問題があります。Dはこれを改善するため、値範囲伝搬(Value Range Propagation)を導入しています。
これを使ったコードは以下の様になります。
byte a, b, c;
c = (a + b) & 0xFF;
0xFF
との論理積が追加されていることに注意してください。(a + b)
の演算結果が何であろうと、論理積の結果は8bitに収まるためbyteへの狭化変換によって情報が失われることはありません。従ってコンパイラはこのコードを安全であると判断し、コンパイルが成功します。
pure関数の詳細な定義
(以前Twitterで発言したもののまとめ: http://togetter.com/li/330590)
コード上では単にpure
属性を付けるだけですが、コンパイラの内部的にはこれは3種類に分類されます。
-
Weak Purity:
pure関数が引数で渡された参照を通して、関数外のデータを書き換えることができる場合。pure int foo(int[] arr);
fooは引数arrが指す先を書き換えることができるので、いわゆる「副作用」を持ちます。
Dが持つ3種類のpureの中で最も弱い保証になります。 -
Constant Purity:
引数がconstな値への参照を含み、かつそれを関数から返す(可能性がある)場合。
たとえば以下の様なpure関数fooが存在した場合、pure const(int)[] foo(const(int)[] arr);
引数arrはfooから返される値に出現しうるので(ここではfooの実装は問題ではない)
たとえば以下の様な呼出しを行った場合、foo返した値が後から変化する可能性があります。int[] arr1 = [1,2,3]; const(int)[] arr2 = foo(arr1); assert(arr2 == [1,2,3]); arr1[0] = 99; assert(arr2 == [1,2,3]); // このassertは成功するとは限らない
関数型言語の言葉で言い換えると、fooの呼び出しについて参照透明性を保証できない、となります。
-
Strong Purity:
引数に不変な値のみを取る場合。pure int foo(int n);
引数nは値コピーで渡されるためfoo関数の呼び出し前後で変化しない
pure immutable(int)[] foo(immutable(int)[] arr);
arrはimmutableな要素を指すので、たとえfooがarrをそのまま返してもfooの呼び出し後に変化したりしない
struct S { immutable int* ptr; } pure int foo(S s);
s.ptr
はimmutableなintを指し、s自体は値コピーになるので、引数sは不変な値になる。Weak Purityのように引数を通して呼び出し側の値を書き換える「副作用」を持たず、また引数が参照を持つ場合も参照先は常にimmutableなので、Constant Purityではできなかった参照透明性が保証できるのがこの種類になります。
呼び出し結果をメモ化できるのもこのタイプです。
メンバ関数にpure属性を付けた場合、暗黙のthisが引数に現れたのと同じものとして扱われます。
class C {
int value;
int foo(int n) pure;
// pure static int foo(C _this, int n); と同じ
// _this.valueを書き換えられることに注意
// →Weak Purityになる
const(C) foo(int n) const pure;
// pure static int foo(const C _this, int n); と同じ
// →Constant Purityになる
immutable(int)[] foo(immutable(int)[] arr) immutable pure;
// pure static int foo(immutable C _this, immutable(int)[] arr); と同じ
// →Strong Purityになる
}
私的には、Strong Purityの良さは関数型言語の参照透明性を副作用バリバリの手続き型言語に持ち込める点、Weak Purityの良さはコードの読みやすさの向上(グローバルな状態を考慮しなくてよくなる)だと思います。
====
如何だったでしょうか。後半に行くほど実装がまだ不完全な感もありますが、D言語の力を感じていただければ幸いです。
明日13日は@k_hanazukiさんです。