その他の関数
関数については、この本ではこれまでの章で次のように説明している。
この章では、関数のその他の機能について説明する。
戻り値の型属性
関数は、auto
、ref
、inout
、およびauto ref
としてマークすることができる。これらの属性は、関数の戻り値の型に関するものである。
auto
関数
auto
関数の戻り値の型は指定する必要はない。
戻り値の型は、return
式からコンパイラによって推測される。result
の型はdouble
なので、add()
の戻り値の型はdouble
になる。
return
式が複数ある場合、関数の戻り値の型はそれらの共通型になる。(共通型については、三項演算子 ?:の章で説明した。) 例えば、int
とdouble
の共通型はdouble
なので、次のauto
関数の戻り値の型もdouble
になる。
ref
関数
通常、関数から返される式は呼び出し元のコンテキストにコピーされる。ref
は、その式を代わりに参照によって返すことを指定する。
例えば、次の関数は、2つのパラメータのうち大きい方を返す。
通常、この関数のパラメータと戻り値の両方がコピーされる。
greater()
の戻り値はresult
にコピーされるため、result
に追加しても、その変数だけに影響し、a
もb
も変更されない。
a | b | 結果 |
---|---|---|
1 | 2 | 12 |
ref
パラメータはコピーされるのではなく、参照によって渡される。同じキーワードは、戻り値にも同じ効果をもたらす。
この場合、返される参照は引数のいずれかのエイリアスとなり、返された参照を変更すると、a
またはb
のいずれかが変更される:
返される参照は直接加算されることに注意。その結果、2つの引数のうち大きい方が変更される。
a | b |
---|---|
1 | 12 |
ローカル参照にはポインタが必要だ。重要な点は、戻り値の型はref
と指定されているが、戻り値がローカル変数に代入されても、a
およびb
は変更されないことだ。
greater()
はa
またはb
への参照を返すが、その参照はローカル変数result
にコピーされるため、a
もb
も変更されない:
a | b | 結果 |
---|---|---|
1 | 2 | 12 |
result
がa
またはb
への参照であるためには、ポインタとして定義する必要がある:
この場合、result
はa
またはb
への参照となり、それを通じた変更は実際の変数に影響を与える:
a | b | 結果 |
---|---|---|
1 | 12 | 12 |
ローカル変数への参照を返すことはできない。 ref
の戻り値は、関数が呼び出される前にその存在を開始した引数の1つへのエイリアスである。つまり、a
またはb
への参照が返されるかどうかに関係なく、返された参照は、まだ存在している変数を指す。
逆に、関数を離れる時点で存在していない変数への参照を返すことはできない。
ローカル変数result
の寿命は、関数を離れると終了する。そのため、その変数への参照を返すことはできない。
auto ref
関数
auto ref
は、上記のparenthesized()
のような関数で役立つ。auto
と同様に、auto ref
関数の戻り値の型はコンパイラによって推測される。さらに、戻り値の式が参照である場合は、その変数はコピーされるのではなく、参照として返される。
parenthesized()
戻り値の型がauto ref
であれば、コンパイルできる。
関数の最初のreturn
文によって、関数がコピーを返すか参照を返すかが決まる。
auto ref
は、テンプレートパラメータがコンテキストに応じて参照またはコピーになる可能性がある関数テンプレートでより有用だ。
inout
関数
inout
キーワードは、関数のパラメータ型および戻り値の型に使用される。これは、const
、immutable
、およびmutableのテンプレートのように機能する。
前の関数を、string
(つまりimmutable(char)[]
) を引数に取り、string
を返す関数に書き直しよう。
予想通り、このコードはstring
引数で動作する:
(こんにちは)
しかし、この関数はimmutable
文字列でしか動作しないため、本来の可能性よりも有用性が低いと言える。
同じ制限は、const(char)[]
文字列にも適用される。
この使いやすさの問題に対する1つの解決策は、const
および変更可能な文字列に対して関数をオーバーロードすることだ。
この設計は、コードの重複が明らかなため、あまり理想的とは言えない。別の解決策としては、関数をテンプレートとして定義することだ。
この方法は機能するが、今回は柔軟性が過剰でテンプレート制約が必要になる可能性があるため、好ましくないかもしれない。
inout
は、テンプレートの解決策とよく似ている。違いは、型全体ではなく、可変性属性だけがパラメータから推論される点だ。
inout
は、推測された可変性属性を戻り値の型に転送する。
char[]
で関数が呼び出されると、inout
がまったく指定されていないかのようにコンパイルされる。一方、immutable(char)[]
またはconst(char)[]
で呼び出されると、inout
はそれぞれimmutable
またはconst
を意味する。
次のコードは、返される式の型を出力して、これを示している。
出力:
char[]
const(char)[]
string
動作属性
pure
、nothrow
、@nogc
は、関数の動作に関するものである。
pure
関数
関数の章で見たように、関数は戻り値と副作用を生成することができる。可能であれば、戻り値を副作用よりも優先すべきである。副作用のない関数は理解しやすく、その結果、プログラムの正確性や保守性が向上するからだ。
同様の概念として、関数の純度がある。Dでは、純度は他のほとんどのプログラミング言語とは異なって定義されている。Dでは、変更可能なグローバル状態やstatic
状態にアクセスしない関数はpureである。(入力および出力ストリームは変更可能なグローバル状態と見なされるため、pure関数は入力または出力操作も実行できない。)
つまり、関数は、そのパラメータ、ローカル変数、および不変のグローバル状態にのみアクセスして、戻り値と副作用を生成する場合、pureであると言える。
Dのpure性の重要な側面は、pure関数はそのパラメータを変化させることができることだ。
さらに、プログラムのグローバル状態を変更する以下の操作は、pure関数では明示的に許可されている。
new
式によるメモリの割り当て- プログラムを終了する
- 浮動小数点処理フラグへのアクセス
- 例外をスローする
pure
キーワードは、関数がこれらの条件に従って動作することを指定し、コンパイラはそれが確実に実行されるように保証する。
当然のことながら、pureでない関数は同じ保証を提供しないため、pure関数はpureでない関数を呼び出すことはできない。
次のプログラムは、pure関数で実行できる操作と実行できない操作の一部を示している。
一部のpure関数は、そのパラメータを変更することは許可されているが、実際には変更しない。pure性の規則に従うと、このような関数の唯一観察可能な効果は、その戻り値だけになる。さらに、この関数は変更可能なグローバル状態にアクセスできないため、プログラムの実行中にその関数がいつ、何回呼び出されたかに関係なく、指定された引数のセットに対して戻り値は同じになる。この事実により、コンパイラとプログラマの両方に最適化の可能性が生まれる。例えば、特定の引数のセットに対して関数を 2 度呼び出す代わりに、1 度目の呼び出しの戻り値をキャッシュして、実際に再び関数を呼び出す代わりにそれを使用することができる。
テンプレートのインスタンス化のために生成される正確なコードは、実際のテンプレート引数によって異なるため、生成されるコードがpureであるかどうかは、引数にも依存する。そのため、テンプレートのpure性は、生成されたコードからコンパイラによって推測される。(pure
キーワードは、プログラマが指定することはできる。)同様に、auto
関数のpure性も推測される。
簡単な例として、N
が0の場合、次の関数テンプレートはpureでないため、pure関数からtempl!0()
を呼び出すことはできない。
コンパイラは、テンプレートの0
のインスタンス化がpureでないことを推測し、pure関数foo()
からの呼び出しを拒否する。
しかし、0以外の値に対するテンプレートのインスタンス化はpureであるため、そのような値に対してはプログラムをコンパイルすることができる。
前述のように、writeln()
のような入力および出力関数は、グローバル状態にアクセスするため、pure関数では使用できない。デバッグ中に一時的にメッセージを表示する必要がある場合など、このような制限が厳しすぎる場合もある。そのため、debug
とマークされたコードについては、pure性のルールが緩和されている。
上記のpure関数は、グローバル変数を変更してメッセージを出力することにより、プログラムのグローバル状態を変更する。これらのpureでない操作にもかかわらず、これらの操作はdebug
とマークされているため、コンパイルは可能だ。
注釈:これらの文は、‑debug
コマンドラインスイッチを使用してプログラムがコンパイルされた場合にのみ、プログラムに含まれることを覚えておいてほしい。
メンバー関数も、pure
とマークすることができる。サブクラスは、pure
として不純な関数をオーバーライドすることができるが、その逆は許可されていない。
デリゲートおよび匿名関数もpureにすることができる。テンプレートと同様に、関数リテラル、デリゲートリテラル、またはauto
関数がpureであるかどうかは、コンパイラによって推測される。
foo()
上記では、そのパラメータがpureなデリゲートである必要がある。コンパイラは、ラムダ式a => 42
がpureであると推測し、foo()
の引数として許容する。ただし、他のデリゲートはpureではないため、foo()
に渡すことはできない。
pure
関数の利点の1つは、その戻り値をimmutable
変数の初期化に使用できることだ。以下のmakeNumbers()
によって生成される配列は変更可能だが、その要素は、その関数の外部にあるコードでは変更できない。そのため、初期化は正常に動作する。
nothrow
関数
関数では、特定のエラー条件下でスローされる例外の型を文書化しておくことが望ましいだろう。ただし、原則として、呼び出し側は、どの関数もどの例外もスローする可能性があることを想定しておく必要がある。
関数がまったく例外を発生させないことを知っておくことがより重要な場合もある。例えば、一部のアルゴリズムは、その手順の一部が例外によって中断されないという事実を利用することができる。
nothrow
関数が例外を発生させないことを保証する。
注釈: Error
およびその基底クラスThrowable
をキャッチすることは推奨されないことを覚えておいてほしい。ここでいう"あらゆる例外"とは、"Exception
階層で定義されているあらゆる例外"を意味する。nothrow
関数は、プログラムの実行を継続できない回復不可能なエラー状態を表すError
階層にある例外を発生させることはできる。
このような関数は、それ自体で例外をスローすることも、例外をスローする可能性のある関数を呼び出すこともできない。
add()
はスローしないという保証に違反しているため、コンパイラはコードを拒否する。
これは、writeln
がnothrow
関数ではない(また、そうなることもできない)ためである。
コンパイラは、関数が例外を発生させることは決してないことを推測できる。nothrow
の以下の実装は、add()
である。これは、try-catch
ブロックによって、例外が関数から逃れることができないことがコンパイラに明らかだからだ。
前述のように、nothrow
には、Error
階層にある例外は含まれない。例えば、[]
を使用して配列の要素にアクセスすると、RangeError
がスローされる可能性があるが、次の関数はnothrow
として定義することができる。
純度と同様に、テンプレート、デリゲート、および匿名関数がnothrow
であるかどうかは、コンパイラが自動的に推測する。
@nogc
関数
Dはガベージコレクション言語だ。ほとんどのDプログラムのデータ構造やアルゴリズムは、ガベージコレクタ(GC)によって管理される動的メモリブロックを利用している。このようなメモリブロックは、ガベージコレクションと呼ばれるアルゴリズムによってGCによって再利用される。
よく使用されるD操作のいくつかは、GCも利用している。例えば、配列の要素は動的メモリブロック上に存在する。
スライスに十分な容量がない場合、上記の~=
演算子はGCから新しいメモリブロックを割り当てる。
GCはデータ構造やアルゴリズムにとって非常に便利な機能だが、メモリの割り当てとガベージコレクションはコストのかかる操作であり、一部のプログラムの実行を著しく遅くする。
@nogc
は、関数がGCを直接または間接的に使用できないことを意味する。
@nogc
関数はGC操作を含まないことをコンパイラが保証する。例えば、次の関数は、@nogc
保証を提供しない上記のappend()
を呼び出すことはできない。
コード安全属性
@safe
、@trusted
、および@system
は、関数が提供するコードの安全性に関する属性だ。純度と同様に、コンパイラはテンプレート、デリゲート、匿名関数、およびauto
関数の安全性のレベルを推論する。
@safe
関数
プログラミングエラーの一種として、意図せずにメモリ内の無関係な場所に書き込み、その場所のデータを破損するエラーがある。このようなエラーは、ほとんどの場合、ポインタの使用や型キャストの適用ミスによって発生する。
@safe
関数は、メモリを破損する可能性のある操作を含まないことを保証する。コンパイラは、@safe
関数で次の操作を許可しない。
- ポインタは、
void*
以外のポインタ型には変換できない。 - ポインタ以外の式は、ポインタ値に変換できない。
- ポインタ値は変更できない (ポインタ演算は不可。ただし、同じ型の別のポインタにポインタを代入することは問題ない)。
- ポインタまたは参照メンバーを持つ共用体は使用できない。
@system
とマークされた関数は呼び出せない。Exception
から派生していない例外はキャッチできない。- インラインアセンブラは使用できない。
- 変更可能な変数は、
immutable
にキャストすることはできない。 immutable
変数はmutableにキャストできない。- スレッドローカル変数は、
shared
にキャストできない。 shared
変数はスレッドローカル型にキャストできない。- 関数ローカル変数のアドレスは取得できない。
__gshared
変数にアクセスすることはできない。
@trusted
関数
一部の関数は、実際には安全であるにもかかわらず、さまざまな理由により@safe
としてマークできない場合がある。例えば、Cで記述されたライブラリを呼び出す必要がある関数では、その言語では安全性がサポートされていない場合がある。
また、@safe
コードでは許可されていない操作を実行する関数もあるが、十分にテストされており、その正しさが信頼されている場合もある。
@trusted
は、関数は@safe
とマークすることはできないが、安全であるとみなすことをコンパイラに伝える属性だ。コンパイラはプログラマを信頼し、@trusted
コードを安全であるかのように扱う。例えば、@safe
コードが@trusted
コードを呼び出すことを許可する。
@system
関数
@safe
または@trusted
とマークされていない関数は、デフォルトの安全属性である@system
とみなされる。
コンパイル時関数実行 (CTFE)
多くのプログラミング言語では、コンパイル時に実行される計算は非常に制限されている。このような計算は、固定長配列の長さを計算したり、単純な算術演算を行うようなシンプルなものである:
上記の1 + 2
式は、3
と記述されたものと同じようにコンパイルされ、実行時には計算は行われない。
DにはCTFEがあり、実行可能であれば、あらゆる関数をコンパイル時に実行することができる。
以下のプログラムは、メニューを出力するプログラムだ。
同じ結果を異なる方法で実現できるものの、上記のプログラムは、以下のstring
を生成するために非自明な操作を実行している:
1 | コーヒー |
2 | 紅茶 |
3 | ホットチョコレート |
drinks
のようなenum
定数の初期値は、コンパイル時に既知でなければならないことを覚えておいてほしい。この事実だけで、menu()
はコンパイル時に実行される。コンパイル時に返される値は、drinks
の初期値として使用される。その結果、その値がプログラムに明示的に記述されているかのように、プログラムがコンパイルされる。
関数がコンパイル時に実行されるためには、その関数がコンパイル時に実際に必要となる式に現れる必要がある。
static
変数の初期化enum
変数の初期化- 固定長配列の長さの計算
- テンプレート値引数の計算
明らかに、すべての関数をコンパイル時に実行することは不可能だ。例えば、グローバル変数にアクセスする関数は、グローバル変数は実行時に初めて存在し始めるため、コンパイル時には実行できない。同様に、stdout
は実行時にのみ利用可能なので、表示を行う関数はコンパイル時には実行できない。
__ctfe
変数
CTFEの強力な点としては、結果が必要になるタイミングに応じて、コンパイル時と実行時の両方で同じ関数を使用できることが挙げられる。CTFEでは、関数を特別な方法で記述する必要はないが、関数内の操作の中には、コンパイル時または実行時のいずれかでしか意味をなさないものもある。特別な変数__ctfe
を使用すると、コンパイル時のみ、または実行時のみに使用するコードを区別することができる。この変数の値は、関数がCTFEで実行されている場合はtrue
、それ以外の場合はfalse
になる。
counter
は実行時にのみ存在するため、コンパイル時には加算することはできない。そのため、上記のコードでは、実行時にのみ加算しようとしている。i
の値はコンパイル時に決定され、j
の値は実行時に決定されるため、foo()
はプログラムの実行中に1回だけ呼び出されたと報告される。
fooが1回呼び出された。
要約
auto
関数の戻り値の型は自動的に推測される。ref
関数の戻り値は、既存の変数への参照である。auto ref
関数の戻り値は、可能であれば参照、それ以外の場合はコピーである。inout
は、パラメータのconst
、immutable
、またはmutable属性を戻り値の型に伝える。pure
関数は、変更可能なグローバル状態や静的状態にアクセスすることはできない。コンパイラは、テンプレート、デリゲート、匿名関数、およびauto
関数の純度を推測する。nothrow
関数は例外を発生させることができない。テンプレート、デリゲート、匿名関数、またはauto
関数がスローしないかどうかは、コンパイラが推測する。@nogc
関数は、GC操作に関与することはできない。@safe
関数はメモリを破損することはできない。コンパイラは、テンプレート、デリゲート、匿名関数、およびauto
関数の安全性属性を推測する。@trusted
関数は確かに安全であるが、そのように指定することはできない。これらは、プログラマとコンパイラの両方によって、@safe
とみなされる。@system
関数は、Dのすべての機能を使用できる。@system
はデフォルトの安全属性だ。- 関数は、コンパイル時にも実行できる(CTFE)。これは、特別な変数
__ctfe
の値によって区別できる。