関数
FunctionBody:
BlockStatement
BodyStatement
InStatement BodyStatement
OutStatement BodyStatement
InStatement OutStatement BodyStatement
OutStatement InStatement BodyStatement
InStatement:
in BlockStatement
OutStatement:
out BlockStatement
out ( Identifier ) BlockStatement
BodyStatement:
body BlockStatement
関数の返値
関数の返値は右辺値とみなされます。 つまり、返値を他の関数に参照渡しすることはできません。
本体なしの関数
本体なしの関数
int foo();
で abstract と宣言されていないものは、どこか他に実装があり、 リンク時に提供されるものと仮定されます。 これによって、ユーザーから関数の実装を完全に隠すことや、 C やアセンブラなど別言語での実装を使うことができます。
pure関数
pure 関数は、 グローバルやstaticの記憶域への書き換えアクセスができず、状態を保存しない関数です。これによって、 渡されたもの以外何も書き換えないことが保証されることを利用した最適化が可能になります。 さらに、コンパイラが pure 関数がその引数を置き換えないと保証できる場合、 完全な、関数としての純粋性 (つまり、 同じ引数に対して常に同じ結果を返す関数であるという保証) が得られます。 そのために、pure関数は以下を満たす必要があります:
- グローバル や static の mutable な状態を読み書きしない
- pure関数はpureでない関数を呼び出すことはできません
- pure関数によるpureでない関数のオーバーライドはできますが、 逆はできません。
- pureでない関数に対し共変です
- I/Oは行えません
実用性を考慮して、pure関数では:
- メモリを NewExpression で割り当てられます
- プログラムを終了できます
- 浮動小数点数の例外フラグを読み書きできます
- 浮動小数点数のモードフラグは、 関数を抜けるときに元の状態に戻すことが保証されている限り、読み書きできます。
- DebugCondition で制御された ConditionalStatement の中では、 pure でない操作を行えます。
pure関数は例外を投げることができます。
import std.stdio;
int x;
immutable int y;
const int* pz;
pure int foo(int i,
char* p,
const char* q,
immutable int* s)
{
debug writeln("in foo()"); // ok, debug文の中ではpureでない関数も可
x = i; // エラー。グローバル状態を書き換えている
i = x; // エラー。mutableなグローバル状態を読み取っている
i = y; // ok。immutableなグローバル状態の読み取り
i = *pz; // エラー。constなグローバル状態を読み取っている
return i;
}
nothrow関数
nothrow関数は、 クラス Exception から派生した例外を一切投げない関数です。
nothrow関数はそうでない関数に対して共変です。
ref関数
ref関数は、返値を参照で返します。 ref引数の返値バージョンです。
ref int foo() {
auto p = new int;
return *p;
}
...
foo() = 3; // ref返値は左辺値となる
auto 関数
auto関数は、関数定義にある ReturnStatement から返値の型を推論します。
auto 関数は返値型なしで宣言されます。 他に特に記憶域クラスのない関数では、 auto 記憶域クラスを指定します。
複数の ReturnStatement がある場合は、 それらの型は完全にマッチしなければいけません。ReturnStatement がない場合は、 返値型は void と推論されます。
auto foo(int i) {
return i + 3; // 返値型はintと推論される
}
auto ref 関数
auto ref 関数は、 auto 関数 と同様に関数の返値型を推論します。 加えて、return される式が左辺値であった場合、 自動的に ref 関数 となります。ただし、値引数やローカル変数への参照を返すようにはなりません。
auto ref foo(int x) { return x; } // 値をreturn
auto ref foo() { return 3; } // 値をreturn
auto ref foo(ref int x) { return x; } // refをreturn
auto ref foo(out int x) { return x; } // refをreturn
auto ref foo() { static int x; return x; } // refをreturn
字句的に最初の ReturnStatement が関数のref性を決めます:
auto ref foo(ref int x) { return 3; return x; } // ok, 値return
auto ref foo(ref int x) { return x; return 3; } // エラー。3は左辺値でない。
inout 関数
mutable, const, immutable のどれに対しても同じように適用したい関数を扱う場合、 その型を返値に伝搬させたいケースが多くあります。
int[] foo(int[] a, int x, int y) { return a[x .. y]; }
const(int)[] foo(const(int)[] a, int x, int y) { return a[x .. y]; }
immutable(int)[] foo(immutable(int)[] a, int x, int y) { return a[x .. y]; }
これらの関数から生成されるコードは完全に一致します。 これらを一つの関数にまとめるには、 inout 型コンストラクタを使います:
inout(int)[] foo(inout(int)[] a, int x, int y) { return a[x .. y]; }
inout は、 mutable, const, immutable のいずれかにマッチするワイルドカードとして働きます。 関数が呼ばれたときに、返値型の inout は、 引数の mutable, const, immutable に合わせて変化します。
inout 型は const に暗黙変換できますが、他の型になることはありません。 他の型を暗黙にinoutに変換することもできません。 inoutへの/からのキャストは @safe 関数では不可能です。
inout が関数引数リストに現れる場合、 返値型にも現れなければいけません。
inout仮引数を持つ関数へと実引数が与えられたとき、以下のケースでマッチしたと判定されます。 実引数も完全に同じinoutを持つか、または:
- 実引数がどれもinout型を持たない
- 仮引数でinout型となっている部分に、実引数側で mutable, const, immutable のいずれかがマッチする。
このようなマッチがあった場合、全てのマッチがmutableなら、inoutはmutalbeとして扱われます。 全てのマッチがimmutableなら、inoutはimmutalbeとして扱われます。 それ以外のケースでは、inoutはconstとして扱われます。 関数の返値の型は、 マッチした属性でinoutを置き換えたものとなります。
グローバルやstaticな変数の型にはinoutを使うことはできません。
注: sharedについて書き忘れたているわけではありません。 shared型はinoutとはマッチしません。
プロパティ関数
@property 属性のついた関数が、プロパティ関数です。 この関数は括弧無しで(つまりプロパティのように) 呼び出すことができます。
struct S {
int m_x;
@property {
int x() { return m_x; }
int x(int newx) { return m_x = newx; }
}
}
void foo() {
S s;
s.x = 3; // s.x(int) を呼ぶ
bar(s.x); // bar(s.x()) を呼ぶ
}
仮想関数
仮想関数とは、 直接呼び出されるのではなく、 vtbl[] と呼ばれるポインタテーブル経由で間接的に呼び出される関数のことをいいます。 全ての非staticかつ非privateかつ非templateなメンバ関数は、 仮想関数です。 これは非効率的に聞こえるかもしれませんが、 コード生成時にDはクラス階層を全て把握していますので、 オーバーライドされていない関数への呼び出しは全て最適化されて non-virtual になります。 C++プログラマは "疑わしきはvirtualにせよ" でコードを書く傾向にありますが、 Dの流儀である "non-virtualにできると証明できない限りvirtualにせよ" の方が、結果的には多くの関数を直接呼び出せます。 オーバーライドしたい関数を 仮想関数にし忘れる、 というバグを減らすことにもつながります。
Dのリンケージを持たない関数はvirtualにはできず、 それゆえオーバーライドできません。
メンバテンプレート関数はvirtualにはできず、 それゆえオーバーライドできません。
final という印のついた関数は、 private でない限り、 派生クラスでオーバーライドできません。例えば:
class A {
int def() { ... }
final int foo() { ... }
final private int bar() { ... }
private int abc() { ... }
}
class B : A {
int def() { ... } // ok。A.defをオーバーライドする。
int foo() { ... } // エラー。A.fooはfinal
int bar() { ... } // ok。A.bar は final private だが virtual でない
int abc() { ... } // ok。A.abc は virtual でない。B.abc は virtual
}
void test(A a) {
a.def(); // B.def を呼ぶ
a.foo(); // A.foo を呼ぶ
a.bar(); // A.bar を呼ぶ
a.abc(); // A.abc を呼ぶ
}
void func() {
B b = new B();
test(b);
}
共変の返値型がサポートされています。 すなわち、派生クラスでオーバーライドした関数では、 基底クラスでの返値型の 派生型オブジェクトを返すことができます。
class A { }
class B : A { }
class Foo {
A test() { return null; }
}
class Bar : Foo {
B test() { return null; } // Foo.test() と共変なオーバーライド
}
仮想関数は全て隠し引数として this 参照を受け取り、 これは関数が呼ばれた対象オブジェクトを指しています。
関数の継承とオーバーライド
派生クラスの関数で、基底クラスの関数と名前と引数の同じものは、 基底の関数を上書きします:class A {
int foo(int x) { ... }
}
class B : A {
override int foo(int x) { ... }
}
void test() {
B b = new B();
bar(b);
}
void bar(A a) {
a.foo(1); // B.foo(int) を呼ぶ
}
しかしながら、オーバーロードの解決の際には、 基底クラスのメンバ関数は考慮されません。
class A {
int foo(int x) { ... }
int foo(long y) { ... }
}
class B : A {
override int foo(long x) { ... }
}
void test() {
B b = new B();
b.foo(1); // B.foo(long) を呼ぶ。A.foo(int) は考慮しない
A a = b;
a.foo(1); // 実行時エラー (A.foo(int) は呼ばれない)
}
オーバーロードの解決の過程で基底クラスの関数も考慮に入れたい場合は、 AliasDeclaration を使います:
class A {
int foo(int x) { ... }
int foo(long y) { ... }
}
class B : A {
alias A.foo foo;
override int foo(long x) { ... }
}
void test() {
B b = new B();
bar(b);
}
void bar(A a) {
a.foo(1); // A.foo(int) を呼ぶ
B b = new B();
b.foo(1); // A.foo(int) を呼ぶ
}
AliasDeclaration を使わなかった場合、 派生クラスの関数は、 例え引数の型が違っていたとしても基底クラスの同名の関数を全てオーバーライドします。 ただし、 実際に基底クラスへの参照を通してそれらの引数型違いの関数が実際に呼び出された場合は、 core.exception.HiddenFuncError 例外が発生します
import core.exception;
class A {
void set(long i) { }
void set(int i) { }
}
class B : A {
void set(long i) { }
}
void foo(A a) {
int i;
try {
a.set(3); // エラー。A.set(int)がBでは定義されていないので
// 実行時に例外を投げる
}
catch (HiddenFuncError o) {
i = 1;
}
assert(i == 1);
}
void main() {
foo(new B);
}
自分のコードを実行してみて HiddenFuncError が送出された場合、 関係するクラスでのオーバーロードやオーバーライドをチェックするようにしましょう。
ただし、 継承階層中の他の全ての仮想関数とオーバーロードに関して排反な場合は、 HiddenFuncError は投げられません。
関数のデフォルト引数値は継承されません:
class A {
void foo(int x = 5) { ... }
}
class B : A {
void foo(int x = 7) { ... }
}
class C : B {
void foo(int x) { ... }
}
void test() {
A a = new A();
a.foo(); // A.foo(5) を呼ぶ
B b = new B();
b.foo(); // B.foo(7) を呼ぶ
C c = new C();
c.foo(); // エラー、C.fooには引数が必要
}
インライン関数
inlineという予約語はありません。 関数をインライン化するかどうかは、 コンパイラが決定します。Cでの予約語registerに対し、 実際に変数をレジスタに配置するかどうかはコンパイラにゆだねられているのと 同様です。(Dにはregisterという予約語もありません。)関数オーバーロード
関数は、仮引数の型と実引数の型がどれだけよくマッチするかという基準によって オーバーロードされます。 もっともよく マッチする関数が選択されます。 マッチの適合度は以下の4段階あります:
- マッチしない
- 暗黙変換によるマッチ
- constへの変換によるマッチ
- 正確なマッチ
実引数それぞれ (this ポインタも含む) が 関数の対応する仮引数と比較され、 その実引数に関するマッチ適合度が決定されます。 引数のうち一番悪いもののマッチ適合度が、 関数全体としてのマッチ適合度と定義されます。
リテラルは ref や out 引数とはマッチしません
複数の関数が同じマッチ適合度にある場合、 partial ordering iによって最適なマッチが決定されます。 Partial ordering では、もっとも特殊化された関数が選択されます。 他と比べてより特殊化されていると言える関数が無い場合、 曖昧な関数呼び出しとしてエラーになります。 関数 f() と g() の partial ordering は、まず f() の引数型を取り出し、 それぞれの型のデフォルト値によって g() の呼び出しの型チェックを試みることで判定されます。 成功すれば、f() は少なくとも g() と同等以上に特殊化されていることになります。 例えば:
class A { }
class B : A { }
class C : B { }
void foo(A);
void foo(B);
void test() {
C c;
/* foo(A) と foo(B) のどちらも暗黙変換によるマッチなので、
* partial ordering を適用する。
* foo(B) は引数 A では呼び出せないが、foo(A) は
* 引数 B で呼び出せる。従って foo(B) の方がより特殊化されていることになり、選択される。
*/
foo(c); // foo(B) を呼び出す
}
可変個引数関数は、 そうでない関数よりも特殊化の度合いが低いと見なされます。
D のリンケージを持たない関数はオーバーロードできません。 名前マングリングが引数の型を考慮していないためです。
オーバーロード集合
同じスコープで宣言された関数同士はお互いオーバーロードしあいます、 このような関数のあつまりを オーバーロード集合 と呼びます。 典型的な例は、 モジュールレベルで定義された関数によるオーバーロード集合です:
module A;
void foo() { }
void foo(long i) { }
A.foo() と A.foo(long) がオーバーロード集合を形成します。 別のモジュールで同名の関数を宣言することも可能で:
module B;
class C { }
void foo(C) { }
void foo(int i) { }
さらに別のモジュール C で A と B を同時に import しても構いません。 C では二つのオーバーロード集合、つまり A.foo oのオーバーロード集合と B.foo oのオーバーロード集合が存在することになります。そして、ちょうど1個のオーバーロード集合に マッチした場合のみ、実際に呼び出される foo のインスタンスが選択されます。
import A;
import B;
void bar(C c) {
foo(); // A.foo() を呼ぶ
foo(1L); // A.foo(long) を呼ぶ
foo(c); // B.foo(C) を呼ぶ
foo(1,2); // エラー。どの foo にもマッチしない
foo(1); // エラー。A.foo(long) と B.foo(int) の両方にマッチ
A.foo(1); // A.foo(long) を呼ぶ
}
foo(1) に対しては B.foo(int) の方が A.foo(long) よりも適合度は高いですが、 違う2つのオーバーロード集合両方にマッチするので、 これはエラーとなります。
alias宣言を使うことで、オーバーロード集合を併合することができます:
import A;
import B;
alias A.foo foo;
alias B.foo foo;
void bar(C c) {
foo(); // A.foo() を呼ぶ
foo(1L); // A.foo(long) を呼ぶ
foo(c); // B.foo(C) を呼ぶ
foo(1,2); // エラー。どの foo にもマッチしない
foo(1); // B.foo(int) を呼ぶ
A.foo(1); // A.foo(long) を呼ぶ
}
関数の引数
関数引数の記憶クラスには in, out, ref, lazy, const, immutable, shared, inout, scope が指定できます。 例:
int foo(in int x, out int y, ref int z, int q);
x は in, y は out, z は ref, q はそのいずれでもありません。
- 関数宣言にどれが入力でどれが出力か明示することで、 理解しやすくなります。
- IDLを別の言語として用意する必要がなくなります。
- コンパイラに多くの情報を与えることで、 よりよいエラーチェックやコード生成が 可能になります。
記憶域クラス | 説明 |
---|---|
none | 仮引数は実引数の書き換え可能なコピーとなる |
in | const scope と同じ |
out | 仮引数は関数に入る際に、 その型のデフォルト値で初期化される |
ref | 引数は参照渡しされる |
scope | 仮引数への参照は関数スコープの外に出すこと (グローバル変数への代入など) はできない |
lazy | 実引数は呼び出し側ではなく、呼ばれた関数の中で使った時に評価される |
const | 実引数が暗黙にconst型に変換される |
immutable | 実引数が暗黙にimmutable型に変換される |
shared | 実引数が暗黙にshared型に変換される |
inout | 実引数が暗黙にinout型に変換される |
void foo(out int x) {
// x は int.init、つまり 0 に
// 関数の最初でセットされる
}
int a = 3;
foo(a);
// a は 0
void abc(out int x) {
x = 2;
}
int y = 3;
abc(y);
// y は 2
void def(ref int x) {
x += 1;
}
int z = 3;
def(z);
// z は 4
動的配列やオブジェクト引数は元々参照渡しされますが、 これらについては、 in/out/ref は参照に対して適用され、中身には影響しません。
lazy 引数は関数が呼び出されるときに評価されるのではなく、 関数の中で引数が使われる時点で初めて評価されます。このため、 lazy 引数は一度も実行されないこともあれば複数回実行される可能性もあります。 lazy 引数は左辺値とすることはできません。
void dotimes(int n, lazy void exp) {
while (n--)
exp();
}
void test() {
int x;
dotimes(3, writefln(x++));
}
次のように表示されます:
0
1
2
void 型の lazy 引数は、任意の型の引数を受け付けます。
デフォルト引数
関数のパラメタ宣言にはデフォルト値を設定することが可能です:
void foo(int x, int y = 3) {
...
}
...
foo(4); // foo(4, 3); と同じ
デフォルト引数は、 関数宣言のコンテキストで評価されます。 引数にデフォルト値を設定する場合、 それよりも後ろの引数にも全てデフォルト値を設定する必要があります。
可変個関数引数
引数の個数が定まっていな関数のことを、可変個引数関数と呼びます。 可変個引数関数には、 以下の3つの形式があります:- Cスタイル可変個引数関数
- Dスタイル可変個引数関数
- 型安全可変個引数関数
Cスタイル可変個引数関数
Cスタイル可変個引数関数は、必須の引数の後ろに ... をつけた形で宣言されます。 extern (C) のような、D以外のリンケージを持ちます:extern (C) int foo(int x, int y, ...);
foo(3, 4); // ok
foo(3, 4, 6.8); // ok, 可変引数が1つ。
foo(2); // エラー。引数yは必須。
この形式の関数は、最低1つの非変引数を受け取る必要があります。
extern (C) int def(...); // エラー、最低一つの引数が必要
Cスタイル可変個引数関数は、Cの可変個引数関数の呼び出し規約と一致していて、 printf のような Cのライブラリ関数を呼び出すのに利用できます。
可変個引数へのアクセスは、 標準ライブラリモジュール core.stdc.stdarg を使います。
import core.stdc.stdarg;
void test() {
foo(3, 4, 5); // 可変個引数の先頭は、5
}
int foo(int x, int y, ...) {
va_list ap;
version (X86_64)
va_start(ap, __va_argsave);
else version (X86)
va_start(ap, y); // y は最後の名前付きメンバ
int z;
va_arg(ap, z); // z は 5 になる
va_end(ap);
}
Dスタイル可変個引数関数
型情報付き可変個引数関数は、必須の引数の後ろに ... をつけた形で宣言します。 この種類の関数はDリンケージを持ち、 固定の引数が一個もなくても構いません:int abc(char c, ...); // 必須引数が一つ: c
int def(...); // ok
引数にアクセスするには、以下の import が必要です:
import core.vararg;
可変個引数関数の中では、
core.vararg で参照できる
特別なローカル変数 _argptr
が使えます。
引数にアクセスするには、_argptr を
va_arg と合わせて使います。
import core.vararg;
void test() {
foo(3, 4, 5); // 可変個引数の先頭は、5
}
int foo(int x, int y, ...) {
int z;
z = va_arg!int(_argptr); // z に 5 が入る
}
Dのリンケージを持つ可変個引数関数には、さらに TypeInfo[]
型の隠れた引数 _arguments が渡されます。
_arguments によって、
実際に渡された引数の個数とそれぞれ型がわかります。
この情報を元に、型安全な可変個引数関数を作ることができます。
import std.stdio;
import core.vararg;
class Foo { int x = 3; }
class Bar { long y = 4; }
void printargs(int x, ...) {
writefln("%d arguments", _arguments.length);
for (int i = 0; i < _arguments.length; i++)
{
writeln(_arguments[i]);
if (_arguments[i] == typeid(int))
{
int j = va_arg!(int)(_argptr);
writefln("\t%d", j);
}
else if (_arguments[i] == typeid(long))
{
long j = va_arg!(long)(_argptr);
writefln("\t%d", j);
}
else if (_arguments[i] == typeid(double))
{
double d = va_arg!(double)(_argptr);
writefln("\t%g", d);
}
else if (_arguments[i] == typeid(Foo))
{
Foo f = va_arg!(Foo)(_argptr);
writefln("\t%s", f);
}
else if (_arguments[i] == typeid(Bar))
{
Bar b = va_arg!(Bar)(_argptr);
writefln("\t%s", b);
}
else
assert(0);
}
}
void main() {
Foo f = new Foo();
Bar b = new Bar();
writefln("%s", f);
printargs(1, 2, 3L, 4.5, f, b);
}
これは次のように表示されます:
00870FE0
5 arguments
int
2
long
3
double
4.5
Foo
00870FE0
Bar
00870FD0
型安全可変個引数関数
型安全可変個引数関数は、 引数の可変部分が配列やクラスオブジェクトの生成に使われる、 という形で実現されています。配列として可変部分を受け取る場合:
int test() {
return sum(1, 2, 3) + sum(); // 6+0 を返す
}
int func() {
int[3] ii = [4, 5, 6];
return sum(ii); // 15を返す
}
int sum(int[] ar ...) {
int s;
foreach (int x; ar)
s += x;
return s;
}
静的配列の場合:
int test() {
return sum(2, 3); // エラー。配列に3個の値が必要
return sum(1, 2, 3); // 6を返す
}
int func() {
int[3] ii = [4, 5, 6];
int[] jj = ii;
return sum(ii); // 15を返す
return sum(jj); // エラー、型の不一致
}
int sum(int[3] ar ...) {
int s;
foreach (int x; ar)
s += x;
return s;
}
クラスオブジェクトの場合:
class Foo {
int x;
string s;
this(int x, string s) {
this.x = x;
this.s = s;
}
}
void test(int x, Foo f ...);
...
Foo g = new Foo(3, "abc");
test(1, g); // ok, gはFooのインスタンス
test(1, 4, "def"); // ok
test(1, 5); // エラー、Fooのコンストラクタでマッチするものがない
実装は、オブジェクトや配列のインスタンスをスタック上に作ることができます。
ということは、可変個引数関数からreturnした後にそのインスタンスを参照すると、
エラーになります:
Foo test(Foo f ...) {
return f; // エラー。インスタンスfの内容はreturnのあと無効になる
}
int[] test(int[] a ...) {
return a; // エラー。returnの後は配列の内容は無効
return a[0..1]; // エラー。returnの後は配列の内容は無効
return a.dup; // ok, コピーが作られている
}
その他の型の場合、引数はその型そのものになります:
int test(int i ...) {
return i;
}
...
test(3); // 3を返す
test(3, 4); // エラー、引数が多すぎる
int[] x;
test(x); // エラー。型の不一致
遅延可変個引数
可変個引数の型が、 引数を持たないdelegateの配列であった場合:
void foo(int delegate()[] dgs ...);
delegate型でないような実引数は、 その型のdelegateへ暗黙変換されます。
int delegate() dg;
foo(1, 3+x, dg, cast(int delegate())null);
これは以下と同じです:
foo( { return 1; }, { return 3+x; }, dg, null );
ローカル変数
ローカル変数に値を代入する前に使用するのはエラーです。実装は、 このエラーを必ずしも検出しないかもしれません。 他の言語のコンパイラはしばしばこの問題に対して警告を出しますが、 これは常にバグなので、エラーであるべきです。
一度も参照されないローカル変数を宣言するのもエラーです。 古い利用されてないコードに現れるような死んだ変数は、 メンテナンスプログラマの混乱の元でしかありません。
同じ関数内の他の変数の名前を隠すローカル変数は、 エラーです:
void func(int x) {
int x; // エラー, xの前の定義を隠している
double y;
...
{ char y; // エラー, yの前の定義を隠している
int z;
}
{ wchar z; // 問題なし, zの前の定義はスコープ外
}
}
これは不合理に見えるかもしれませんが、実際上は、 このエラーに該当するのはバグのあるコードか、 少なくともバグのように見えるコードです。
ローカル変数のアドレスや参照を返すのは、 エラーです。
ローカル変数とラベルとに同じ名前を付けるのはエラーです。
ローカルstatic変数
関数内のローカル変数は static または __gshared と宣言することができ、 その場合変数はスタック上ではなく静的に割り当てられます。 代入された値は関数から一度抜けたあとも保持されるようになります。
void foo() {
static int n;
if (++n == 100)
writeln("called 100 times");
}
static 変数の初期値はコンパイル時評価可能である必要があり、 この初期化はスレッドの開始時 (あるいは __gshared に関してはプログラム開始時) に行われます。 ローカル static 変数に対する静的コンストラクタや静的デストラクタはありません。
static変数の可視性は通常のスコープ規則に従いますが、 その名前は関数内で一意でなければいけません。
void main() {
{ static int x; }
{ static int x; } // エラー
{ int i; }
{ int i; } // ok
}
ネスト関数
他の関数の内部にネストして関数を書くことができます:
int bar(int a) {
int foo(int b) {
int abc() { return 1; }
return b + abc();
}
return foo(a);
}
void test() {
int i = bar(3); // i に 4 が代入される
}
ネスト関数は、その名前がスコープにある範囲からのみ参照できます。
void foo()
{
void A()
{
B(); // エラー。Bは前方参照
C(); // エラー。Cは未定義
}
void B()
{
A(); // ok, スコープ内
void C()
{
void D()
{
A(); // ok
B(); // ok
C(); // ok
D(); // ok
}
}
}
A(); // ok
B(); // ok
C(); // エラー。C は未定義
}
そして:
int bar(int a) {
int foo(int b) { return b + 1; }
int abc(int b) { return foo(b); } // ok
return foo(a);
}
void test() {
int i = bar(3); // ok
int j = bar.foo(3); // エラー。bar.foo はここから見えない
}
ネスト関数は外側の関数の変数や、 その他のシンボルにアクセスできます。 読み書き双方のアクセスが可能です。
int bar(int a) {
int c = 3;
int foo(int b) {
b += c; // 4 が b に足される
c++; // bar.c は 5 になる
return b + c; // 返値は 12
}
c = 4;
int i = foo(a); // i は 12
return i + c; // 17 を返す
}
void test() {
int i = bar(3); // i に 17 が代入される
}
このアクセスは、ネストの階層が複数段でも構いません:
int bar(int a) {
int c = 3;
int foo(int b) {
int abc() {
return c; // bar.c にアクセス
}
return b + c + abc();
}
return foo(3);
}
staticなネストした関数は、外側の関数のスタック変数を触ることができず、 static変数だけにアクセスできます。 これはstaticメンバ関数のふるまいとよく似ています。
int bar(int a) {
int c;
static int d;
static int foo(int b) {
b = d; // ok
b = c; // エラー。foo() は bar() のスタックフレームにアクセスできない
return b + 1;
}
return foo(a);
}
関数は、メンバ関数の中にもネストできます:
struct Foo {
int a;
int bar() {
int c;
int foo() {
return c + a;
}
return 0;
}
}
ネストした関数は、常にDの関数リンケージを持ちます。
モジュールレベルの定義と違い、関数スコープ内での宣言は、 書かれた順番に処理されます。どういうことかと言うと、 2つのネストした関数が相互に呼び出し合うことはできません:
void test() {
void foo() { bar(); } // エラー、bar は未定義
void bar() { foo(); } // ok
}
これは二つの関数をネスト構造体のメンバとして解決する手が一つあります。 あるいは、delegateを使う方法もあります:
void test() {
void delegate() fp;
void foo() { fp(); }
void bar() { foo(); }
fp = &bar;
}
ネスト関数はオーバーロードできません。
デリゲート, 関数ポインタ, クロージャ
関数ポインタは、staticなネストした関数を指すことができます:
int function() fp;
void test() {
static int a = 7;
static int foo() { return a + 3; }
fp = &foo;
}
void bar() {
test();
int i = fp(); // i は 10 となる
}
delegate では、staticでないネストした関数を指せます:
int delegate() dg;
void test() {
int a = 7;
int foo() { return a + 3; }
dg = &foo;
int i = dg(); // i は 10 になる
}
ネストした関数から参照されているスタック変数は、 関数が終了した後も有効です(これは D 1.0 とは異なる仕様です)。これを クロージャ といいます。 ただし、スタック変数のアドレスをreturnするのはクロージャではないため、 エラーになります。
int* bar() {
int b;
test();
int i = dg(); // ok, test.a はクロージャの中にありまだ有効
return &b; // エラー、bar.b は bar() の終了後は無効
}
非staticなネストした関数へのdelegateは、 二つのデータを持ちます。 それは、外側の関数のスタックフレームを指すポインタ (フレームポインタと呼びます) と、 関数のアドレスです。これは、構造体/クラスの 非staticメンバ関数へのdelgateが this ポインタとメンバ関数のアドレスから なっていることと似ています。どちらのタイプのdelegateも互換性があり、 実際に、同じ型です:
struct Foo {
int a = 7;
int bar() { return a; }
}
int foo(int delegate() dg) {
return dg() + 1;
}
void test() {
int x = 27;
int abc() { return x; }
Foo f;
int i;
i = foo(&abc); // i は 28 になる
i = foo(&f.bar); // i は 8 になる
}
環境と関数の組み合わせは、 a 動的クロージャ と呼ばれています。
デリゲートの .ptr プロパティは、 void* 型でフレームポインタの値を返します
デリゲートの .funcptr プロパティは、 function型で関数ポインタの値を返します
今後の方向性: 関数ポインタとdelegateの構文を共通にして、 相互に互換性を持たせる予定です。
無名関数・無名デリゲート
See FunctionLiteral を参照してください。
main() 関数
コンソールプログラムでは、main() が実行開始点となります。 全てのモジュール初期化子が呼ばれ、 ユニットテストが実行された後に呼び出されます。 mainから戻った後は、全てのモジュールデストラクタが呼び出されます。 main() の宣言は必ず以下のいずれかとします:
void main() { ... }
void main(string[] args) { ... }
int main() { ... }
int main(string[] args) { ... }
コンパイル時関数実行 (CTFE)
関数の一部は、コンパイル時に実行可能です。 再帰やループが必要なアルゴリズムで生成される定数をたたみ込みたい時に、 この機能は有用です。 コンパイル時に実行するためには、関数は以下の条件を満たしている必要があります:
- 関数のソースコードがコンパイラに見えている必要があります。 extern で宣言だけが見えている関数はコンパイル時実行はできません。
- 実行される式は、 グローバル変数やローカルstatic変数には触れません。
- asm 文は使えません。
- 移植性のないキャスト (int[] から float[] へのキャストなど)、 具体的にはエンディアンに依存するキャストなどは、使えません。 signed と unsigned の間のキャストは可能です。
CTFEでは、ポインタは、安全な使用であれば使うことができます:
- ポインタ算術に関してはC形式の意味論が厳密に強制されます。 ポインタ算術は静的ないしは動的配列の要素を指すポインタに関してのみ可能です。 このようなポインタは配列の要素か、 配列の終端の1つ先だけを指すことができます。 null ポインタや、配列以外を指しているポインタについては、 ポインタ算術は完全に禁止となります。
- 異なるメモリブロックに属するメモリの配置関係は未定義です。 ポインタの順序比較 (<, <=, >, >=) は同じ配列を指すポインタ間か、または少なくとも一方が null である時のみ可能です。
- 独立したメモリブロック間のポインタ比較は、
そのような比較が
&& か || を組み合わせて結果がメモリブロック間配置に非依存になっている場合を除き、
コンパイル時エラーとなります。それぞれの比較は、二つのポインタ式の間でなされ、
<, <=, >, >= のいずれかで比較したもの、
! によって否定をつけたものが記述可能です。
例として、 (p1 > q1 && p2 <= q2) という式は p1 と p2 がメモリブロック P へのポインタとなる式で、q1 と q2 がメモリブロック Q へのポインタとなる式であれば、P と Q が無関係であっても正統です。 この式は、[p1..p2] が [q1..q2] の内側にあれば true、なければ false を返します。 同様に (p1 < q1 || p2 > q2) という式は [p1..p2] が [q1..q2] の外側にあるときに true となります。
- 等価性比較 (==, !=, is, !is) は全てのポインタの間で、制限無く使用可能です。
- どんなポインタも void * へキャストし、また元の型へと void * からキャストで戻すことができます。 ポインタと非ポインタの間のキャストは許されません。
以上の制限は、実際に実行される式にだけ適用されます。 例えば:
static int y = 0;
int countTen(int x) {
if (x > 10)
++y;
return x;
}
static assert(countTen(6) == 6); // OK
static assert(countTen(12) == 12); // 不可、yを変更しようとする
bool型の __ctfe 擬似変数が提供され、コンパイル時には true に、実行時には false に評価されます。 これによって、 コンパイル時には使えない処理を避ける代替実装を選択することなどができます。
処理をコンパイル時に走らせるには、関数呼び出しが、 コンパイル時に必ず評価されねばならない文脈に現れるようにします。 例として:
- static変数の初期化
- static配列の次元
- テンプレートの引数
template eval( A... ) {
const typeof(A[0]) eval = A[0];
}
int square(int i) {
return i * i;
}
void foo() {
static j = square(3); // コンパイル時
writefln(j);
writefln(square(4)); // 実行時
writefln(eval!(square(5))); // コンパイル時
}
関数のコンパイル時実行は、 ランタイム実行よりもかなり時間がかかります。 関数が無限ループするような場合、実行時ではなく、 コンパイル時にハングします。
修復不可能なエラー (assert 失敗など) は例外を投げません; かわりに、即座に解釈実行が終了されます。
コンパイル時実行される関数は、以下のような状況では、 実行時と異なる値を返すことがあります:
- 浮動小数点演算が、 実行時より高い精度で行われる
- 実装定義の評価順序に依存している
- 未初期化変数の使用
これらは、 最適化の設定が結果に影響を及ぼすのと同じ種類の状況です。
文字列ミックスインとコンパイル時間数実行
コンパイル時に実行される関数は、実行時にも必ず動作するものでなければいけません。 コンパイル時の関数実行は、 必ずしも実行時と同等のものであるとは限りません。 これは、関数のセマンティクスは関数のコンパイル時の値に依存してはいけない、 ということを意味しています。例を挙げると:
int foo(char[] s) {
return mixin(s);
}
const int x = foo("1");
この例は、foo() の実行時コードが生成できないため不正です。 このようなことをしたい場合は、 関数テンプレートを使う方が適切でしょう。
関数の安全性
セーフ関数 は、 未定義動作. を決して引き起こさないことが静的にチェックされた関数です。 未定義動作は、 しばしば悪意のある攻撃の対象となります。
セーフ関数
@safe 属性のついた関数が、セーフ関数です。
セーフ関数の中では、 以下の操作は禁止されます:
- ポインタ型から、void* 以外の他の型へのキャスト
- 非ポインタ型からポインタ型へのキャスト
- ポインタ値の変更
- ポインタと他の型のオーバーラップしたunionへのアクセス
- システム関数の呼び出し
- class Exception 派生でない例外のキャッチ
- インラインアセンブラ
- mutableからimmutableへの明示キャスト
- immutableからmutableへの明示キャスト
- スレッドローカルかっらsharedへの明示キャスト
- sharedからスレッドローカルへの明示キャスト
- ローカル変数や関数引数のアドレス取得
- __gshared 変数へのアクセス
セーフ関数の中にネストした関数は、 デフォルトでセーフ関数とされます。
セーフ関数は信頼済み関数やシステム関数に対して共変です。
注: 検証可能な安全性であるにも関わらず、 コンパイラや仕様のバグによって、検査しきれていない物があるかもしれません。 そのようなエラーを見つけたら、修正できるように、ご報告下さい。
信頼済み関数
@trusted 属性のついた関数が、信頼済み関数です。
信頼済み関数は、 セーフ関数から呼ばれた時には未定義動作を決して引き起こさない、とプログラマによって保証された関数です。 一般に、信頼済み関数は、目で見て安全性を簡単に確認できるように、 できるだけ小さく留めるべきです。
信頼済み関数からは、セーフ・信頼済み・システムのいずれの種類の関数も呼び出せます。
信頼済み関数は、セーフ関数やシステム関数に対して共変です。
システム関数
@safe も @trusted もついておらず、 @safe 関数内のネスト関数でもない関数が、システム関数です。 明示的に、システム関数に @system という属性をつけることもできます。 システム関数はすべて安全性がない、とは限りません。 単に、未定義動作を引き起こさないことのコンパイラによる自動保証はできない、 ということを意味します。
システム関数は、信頼済み関数やセーフ関数に対して共変ではありません
関数属性の推論
FunctionLiteral と 関数テンプレート では、関数本体が常に見えるため、 明示指定して上書きしない限り、 pure, nothrow, @safe 属性が自動で推論されます。
属性の推論は、関数本体が書かれている場合であっても、 そのほかの関数に対しては行われません。
推論は、 関数本体がそれぞれの属性の規則に従っているかを検査することで行われます。
循環する関数 (つまり、 直接あるいは間接的に自身を呼び出す関数) は、impure, throwing, @system, と推論されます。
関数内で自分自身のこれらの属性をテストしようとした場合は、 その属性を持っていない物として推論されます。