関数ポインタ、デリゲート、およびラムダ
関数ポインタは、後でその関数を実行するために、関数のアドレスを格納するためのものだ。関数ポインタは、C プログラミング言語の関数ポインタと似ている。
デリゲートは、関数ポインタと、その関数ポインタを実行するためのコンテキストの両方を格納する。格納されるコンテキストは、関数の実行が行われるスコープ、またはstruct
またはclass
オブジェクトのいずれかだ。
デリゲートは、ほとんどの関数型プログラミング言語でサポートされている概念であるクロージャも実現する。
関数ポインタ
前の章で、&
演算子を使用して関数のアドレスを取得できることを学んだ。その例の1つでは、そのようなアドレスを関数テンプレートに渡した。
テンプレート型パラメータは任意の型と一致できることを利用して、関数ポインタをテンプレートに渡して、その.stringof
プロパティを出力してその型を確認しよう。
プログラムの出力は、myFunction()
の型とアドレスを表している。
型 | int function(char c, double d) |
---|---|
値 | 406948 |
メンバー関数ポインタ
メンバー関数のアドレスは、型または型のオブジェクトのいずれかで取得できるが、結果は異なる。
上記の2つのstatic assert
行から、f
はfunction
であり、d
はdelegate
であることがわかる。後で説明する通り、d
は直接呼び出せるが、f
は呼び出す対象となるオブジェクトが必要だ。
定義
通常のポインタと同様に、各関数ポインタ型は特定の関数型を正確に指すことができる。関数ポインタと関数のパラメータリストおよび戻り値の型は一致する必要がある。関数ポインタは、その特定の型の戻り値の型とパラメータリストの間にfunction
キーワードを挿入して定義する。
パラメータの名前(上記の出力ではc
およびd
)はオプションだ。myFunction()
はchar
およびdouble
を受け取り、int
を返すため、myFunction()
を指すことができる関数ポインタの型は、それに応じて定義する必要がある。
上記の行は、ptr
を、2つのパラメータ (char
およびdouble
) を取り、int
を返す関数ポインタとして定義している。その値は、myFunction()
のアドレスだ。
関数ポインタの構文は比較的読みにくいので、alias
を使用してコードを読みやすくすることが一般的である。
このエイリアスを使用すると、コードが読みやすくなる:
他の型と同様に、auto
も使用できる。
関数ポインタの呼び出し
関数ポインタは、関数とまったく同じように呼び出すことができる。
上記の呼び出しptr('a', 5.67)
は、実際の関数myFunction('a', 5.67)
を呼び出すことと同じである。
使用する場合
関数ポインタは、呼び出す関数を格納し、それが指す関数とまったく同じように呼び出されるため、関数ポインタは、プログラムの動作を後で使用するために効果的に格納する。
Employee
Dには、プログラムの動作に関する他の多くの機能がある。例えば、従業員の給与を計算するために呼び出す適切な関数は、enum
メンバーの値によって決定することができる。
残念ながら、この方法は、既知のすべての従業員型をサポートしなければならないため、メンテナンスが比較的困難だ。新しい従業員型がプログラムに追加された場合、そのようなswitch
文をすべて見つけ、新しい従業員型用の新しいcase
句を追加する必要がある。
動作の違いを実装するより一般的な代替手段は、多型だ。Employee
インターフェースを定義し、そのインターフェースのさまざまな実装によってさまざまな給与の計算を処理することができる。
関数ポインタも、異なる動作を実装するためのもう1つの代替手段である。これらは、オブジェクト指向プログラミングをサポートしていないプログラミング言語でよく使用される。
パラメータとしての関数ポインタ
配列を受け取り、別の配列を返す関数を設計しよう。この関数は、値が 0 以下の要素をフィルタリングし、それ以外の要素に10を乗算する。
次のプログラムは、ランダムに生成された値を使用してその動作を示している。
出力には、元の値が0より大きい数値が元の値の10倍になった数値が含まれている。選択された元の数値はハイライト表示されている:
入力 | [-2, 2, -2, 3, -2, 2, -1, -4, 0, 0] |
---|---|
出力 | [20, 30, 20] |
filterAndConvert()
これは非常に特定のタスク向けだ:常に0より大きい数値を選択し、常にそれらを10倍する。フィルタリングと変換の動作をパラメーター化すれば、より有用になるかもしれない。
フィルタリングも変換の一種(int
からbool
へ)であることを考慮すると、filterAndConvert()
は2つの変換を実行していることになる。
number > 0
これは、int
の値を考慮してbool
を生成する。number * 10
これは、int
の値からint
を生成する。
上記の2つの変換に一致する関数ポインタの便利なエイリアスを定義しよう。
Predicate
は、int
を受け取り、bool
を返す関数の型で、Convertor
は、int
を受け取り、int
を返す関数の型である。
このような関数ポインタをパラメータとして指定すると、filterAndConvert()
はその動作中にこれらの関数ポインタを使用することができる。
filterAndConvert()
これで、実際のフィルタリングおよび変換操作に依存しないアルゴリズムができた。必要に応じて、以前の動作は次の2つの単純な関数によって実現できる。
この設計により、任意のフィルタリングおよび変換動作を持つfilterAndConvert()
を呼び出すことができる。例えば、次の2つの関数を使用すると、filterAndConvert()
は偶数の負の値を生成する。
出力:
入力 | [3, -3, 2, 1, -5, 1, 2, 3, 4, -4] |
---|---|
出力 | [-2, -2, -4, 4] |
これらの例でわかるように、このような関数は非常に単純であるため、名前、戻り値の型、パラメータのリスト、および中括弧で囲んだ適切な関数として定義するのは不必要に冗長だ。
以下で見るように、=>
の匿名関数の構文を使用すると、コードがより簡潔で読みやすくなる。次の行には、isEven()
およびnegativeOf()
と同等の匿名関数が、適切な関数定義なしで記述されている。
メンバーとしての関数ポインタ
関数ポインタは、構造体やクラスのメンバーとしても格納できる。これを確認するために、後で使用するために、述語と変換子をコンストラクタのパラメータとして受け取るclass
を設計しよう。
この型のオブジェクトは、filterAndConvert()
と同様に使うことができる。
関数
短い関数を適切な関数定義なしで定義すると、コードがより読みやすく簡潔になる。
関数リテラル またはラムダとも呼ばれる匿名関数を使用すると、式の中で関数を定義することができる。匿名関数は、関数ポインタを使用できる場所ならどこでも使用することができる。
そのより短い=>
構文については、後で説明する。まず、完全な構文を見てみよう。これは、特に他の式の中に現れる場合、通常、あまりにも冗長になる。
例えば、2より大きい数値の7倍を生成するNumberHandler
オブジェクトは、次のコードのように、匿名関数を使用して構築することができる。
上記のコードの2つの利点は、関数が適切な関数として定義されていないことと、その実装がNumberHandler
オブジェクトが構築された場所で直接確認できることである。
匿名関数の構文は、通常の関数の構文とよく似ていることに注意。この一貫性には利点があるが、匿名関数の完全な構文はコードを冗長にしてしまいる。
そのため、匿名関数を定義するより短い方法がいくつかある。
より短い構文
戻り値の型が、匿名関数内のreturn
文から推測できる場合は、戻り値の型を指定する必要はない (戻り値の型が通常表示される場所は、コードコメントで強調表示されている)。
さらに、匿名関数がパラメータを受け取らない場合、そのパラメータリストを指定する必要はない。何も受け取らず、double
を返す関数ポインタを受け取る関数を考えてみよう。
その関数に渡される匿名関数は、空のパラメータリストを持つ必要はない。したがって、次の3つの匿名関数の構文はすべて同等である。
1つ目は完全な構文で記述されている。2つ目は、戻り値の型推論を利用して、戻り値の型を省略している。3つ目は、不要なパラメータリストを省略している。
さらに、キーワードfunction
も指定する必要はない。その場合、それが匿名関数か匿名デリゲートかを判断するのはコンパイラに任される。囲んでいるスコープの変数を使用しない限り、それは関数になる。
ほとんどの匿名関数は、ラムダ構文を使用することでさらに短く定義できる。
単一のreturn
文の代わりにラムダ構文を使用
ほとんどの場合、上記の最も短い構文でさえも不必要に煩雑だ。関数パラメータリストのすぐ内側に中括弧があることでコードが読みにくくなり、関数引数内のreturn
文とそのセミコロンが場違いに見える。
return
ステートメントが1つだけある匿名関数の完全な構文から始めよう。
function
キーワードは不要であり、戻り値の型は推測できることはすでに説明した。
その定義の同等物は、次の=>
構文で表すことができる。ここで、=>
文字は波括弧、return
キーワード、およびセミコロンを置き換える:
この構文の意味は、"これらのパラメータが与えられた場合、この式 (値) を生成する"と表現できる。
さらに、パラメーターが1つの場合、パラメーターリストの周囲の括弧も省略できる:
一方、文法上の曖昧さを避けるため、パラメーターが全くない場合でも、パラメーターリストは空の丸括弧で表す必要がある:
他の言語でラムダ式を知っているプログラマは、=>
の後に中括弧を使用してしまうミスを犯す可能性がある。これは、別の意味を持つD構文として有効である。
std.algorithm.filter
に渡される述語でラムダ構文を使ってみよう。filter()
は、テンプレートパラメータとして述語、関数パラメータとして範囲を受け取る。述語を範囲の各要素に適用し、述語を満たす要素を返す。述語を指定するいくつかの方法のうちの1つがラムダ構文だ。
(注釈: 範囲については後の章で説明する。この時点では、Dスライスは範囲であるということを知っておけば十分だ。)
次のラムダ式は、10より大きい要素に一致する述語である:
出力には、述語を満たす要素のみが含まれる:
[20, 300]
比較のために、同じラムダ式を最長構文で記述しよう。匿名関数の本体を定義する中括弧が強調表示されている。
別の例として、今回は2つのパラメータを取る匿名関数を定義しよう。次のアルゴリズムは、2つのスライスを受け取り、それに対応する要素を1つずつ、2つのパラメータを取るfunction
に渡す。そして、結果を別のスライスとして収集して返す。
上記のfunction
パラメーターが2つのパラメーターを取るため、binaryAlgorithm()
に渡すラムダ式も2つのパラメーターを取らなければならない:
出力には、最初の配列の要素の10倍に2番目の配列の要素を加えたものが含まれる(例:14は10 × 1 + 4):
[14, 25, 36]
デリゲート
デリゲートは、関数ポインタと、その関数が実行されるコンテキストとの組み合わせだ。デリゲートはDでもクロージャをサポートしている。クロージャは、多くの関数型プログラミング言語でサポートされている機能だ。
ライフタイムと基本操作の章で見たように、変数のライフタイムは、それが定義されたスコープを離れると終了する:
そのため、このようなローカル変数のアドレスは関数から返すことができない。
increment
が、function
を返す関数のローカル変数であると想像しよう。返されるラムダが、そのローカル変数を使用するようにしよう。
このコードはエラーである。返されたラムダ式が、スコープ外に出ようとしているローカル変数を使用しているからだ。このコードがコンパイル可能だった場合、ラムダ式はincrement
にアクセスしようとし、その変数のライフタイムは既に終了しているため、エラーが発生する。
このコードをコンパイルして正しく動作させるには、increment
の有効期間が、それを使用するラムダの有効期間以上である必要がある。デリゲートは、関数が使用するローカル状態が有効であり続けるように、ラムダのコンテキストの有効期間を延長する。
delegate
function
の構文と似ているが、唯一の違いはキーワードである。この変更だけで、以前のコードがコンパイル可能になる:
デリゲートによって使用されたローカル変数increment
は、そのデリゲートが存続する限り存続する。この変数は、他の変数と同様にデリゲートから利用でき、必要に応じて変更可能だ。この例については、次の章で、opApply()
メンバー関数とデリゲートを使用する場合に説明する。
以下のコードは、上記のデリゲートをテストするコードだ:
makeCalculator()
は、匿名デリゲートを返すことに注意。上記のコードは、そのデリゲートを変数calculator
に割り当て、calculator(3)
で呼び出している。デリゲートは、そのパラメータと変数increment
の和を返すように実装されているため、コードは3と10の和を出力する。
計算結果 | 13 |
---|
より短い構文
前の例で既に使用したように、デリゲートはより短い構文も利用できる。function
もdelegate
も指定されていない場合、ラムダの型は、ラムダがローカル状態にアクセスするかどうかによって、コンパイラによって決定される。アクセスする場合は、delegate
になる。
次の例は、パラメータを受け取らないデリゲートがある。
関数delimitedNumbers()
は、最初と最後の要素が-1のスライスを生成する。この関数は、最初と最後の要素の間にくる他の要素を指定する2つのパラメータを取る。
常に同じ値を返す単純なデリゲートを使って、この関数を呼び出そう。パラメータがない場合、ラムダのパラメータリストは空として指定する必要があることを覚えておいてほしい。
出力:
-1 | 42 | 42 | 42 | -1 |
今度は、ローカル変数を使用するデリゲートでdelimitedNumbers()
を呼び出そう:
このデリゲートはランダムな値を生成するが、値は最後の値に追加されるため、生成される値はいずれも前の値よりも小さくならない。
-1 | 0 | 2 | 3 | 4 | 6 | 6 | 8 | 9 | 9 | 9 | 10 | 12 | 14 | 15 | 17 | -1 |
最後の数字 | 17 |
---|
オブジェクトとメンバー関数をデリゲートとして
デリゲートは、関数ポインタとそれが実行されるコンテキストにすぎないことをこれまで見てきた。この2つの代わりに、デリゲートは、メンバー関数と、そのメンバー関数が呼び出される既存のオブジェクトで構成することもできる。
オブジェクトからこのようなデリゲートを定義する構文は次の通りだ:
まず、この構文が実際にdelegate
を定義していることを、string
表現で出力して確認しよう:
出力によると、location
で呼び出されるmoveHorizontally()
の型は、確かにdelegate
である。
void delegate(long step)
&
構文は、デリゲートを構築するためだけのものだということに注意。デリゲートは、後で関数呼び出し構文によって呼び出される。
delegate
は、location
オブジェクトとmoveHorizontally()
メンバー関数を組み合わせたものなので、デリゲートを呼び出すことは、location
でmoveHorizontally()
を呼び出すことと同じになる。出力は、オブジェクトが実際に水平方向に 3 ステップ移動したことを示している。
Location(3, 0)
関数ポインタ、ラムダ、およびデリゲートは式である。これらは、その型の値が期待される場所で使用できる。例えば、delegate
オブジェクトのスライスは、オブジェクトとそのさまざまなメンバー関数から構築されたデリゲートから、以下のように初期化される。スライスのdelegate
要素は、後で関数と同じように呼び出される。
スライスの要素によると、位置は水平方向に2回、垂直方向に1回変更されている:
Location(2, 1)
デリゲートプロパティ
デリゲート関数の関数ポインタとコンテキストポインタは、それぞれ.funcptr
プロパティと.ptr
プロパティからアクセスできる。
これらのプロパティを明示的に設定することで、delegate
を最初から作成することができる。
上記のデリゲートをd()
として呼び出すことは、式o.func()
(つまり、o
でMyStruct.func
を呼び出すこと)と同等だ。
42
遅延パラメーターはデリゲートだ
関数パラメータの章で、lazy
キーワードを見た。
message
は上記のlazy
パラメータであるため、そのパラメータがlog()
内で使用された場合に、format
式全体 (getConnectionState()
の呼び出しを含む)が評価される。
内部的には、遅延パラメーターは実際にはデリゲートであり、遅延パラメーターに渡される引数は、コンパイラーによって自動的に作成されるデリゲートオブジェクトである。以下のコードは、上記のコードと等価だ:
lazy
パラメータはstring
ではなく、string
を返すデリゲートだ。- そのデリゲートが呼び出されて、その戻り値が取得される。
- 式全体がデリゲートで囲まれ、そこから返される。
遅延可変長引数関数
関数に可変数の遅延パラメータが必要な場合、その未知のパラメータ数を lazy
として指定することは必然的に不可能だ。
この問題を解決するには、可変長のdelegate
パラメータを使用する。このようなパラメータは、それらのデリゲートと同じ戻り値の型を持つ式をいくつでも受け取ることができる。デリゲートはパラメータを受け取ることができない。
double
式とラムダ式の両方が可変長引数とどのように一致するかに注意。double
式は自動的にデリゲート内にラップされ、関数はそのすべての事実上遅延パラメータの値を出力する。
1.5
2.5
このメソッドの制限として、すべてのパラメータは同じ型(上記のdouble
)でなければならない。この制限を解除するには、後述のその他のテンプレートの章で、タプルテンプレートパラメータを利用する方法を見ていく。
toString()
delegate
パラメーター付き
この本では、オブジェクトを文字列として表現するために、これまで多くのtoString()
関数を定義してきた。これらのtoString()
定義は、いずれもパラメータを受け取らずにstring
を返していた。以下のコメント行で指摘されているように、構造体およびクラスは、それぞれのメンバーをformat()
に渡すだけで、それぞれのメンバーのtoString()
関数を利用していた。
プログラムの最後の行で、polygon
がstring
として出力されるためには、Polygon
、ColoredPoint
、Color
、およびPoint
のすべてのtoString()
関数が間接的に呼び出され、その過程で合計10個の文字列が作成される。下位レベルの関数によって構築され、返される文字列は、それらを呼び出したそれぞれの上位レベルの関数によって1回だけ使用されることに注意しよう。
ただし、合計で10個の文字列が作成されるものの、出力に表示されるのは最後の1つだけだ。
[{RGB:10,10,10;(1,1)}, {RGB:20,20,20;(2,2)}, {RGB:30,30,30;(3,3)}]
この方法は実用的だが、多くのstring
オブジェクトが構築され、すぐに破棄されるため、プログラムのパフォーマンスが低下する可能性がある。
toString()
のオーバーロードは、delegate
パラメータを取ることでこのパフォーマンス問題を回避する:
宣言からわかるように、このオーバーロードされたtoString()
はstring
を返さない。代わりに、出力される文字列はdelegate
パラメーターに渡しされる。出力される単一のstring
にこれらの文字列をappendする責任は、delegate
にある。
プログラマーが変更しなければならないのは、std.string.format
の代わりにstd.format.formattedWrite
を呼び出し、delegate
パラメータを最初のパラメータとして渡すことだけだ(以下のUFCSを参照)。また、formattedWrite
のコンパイル時のフォーマット文字列チェックを利用するために、以下の呼び出しではフォーマット文字列をテンプレート引数として指定していることに注意。
このプログラムの利点は、さまざまなtoString()
関数に対して合計10件の呼び出しが行われているにもかかわらず、これらの呼び出しはまとめて1つのstring
を生成し、10件生成しないことだ。
要約
function
キーワードは、後で関数と同じように呼び出す関数ポインタを定義するためのものだ。delegate
キーワードは、デリゲートを定義するためのものだ。デリゲートは、関数ポインタとその関数ポインタが実行されるコンテキストのペアだ。delegate
は、&object.member_function
という構文を使用して、オブジェクトとメンバー関数から作成することができる。- デリゲートは、その
.funcptr
および.ptr
プロパティを設定することで明示的に構築することができる。 - 匿名関数および匿名デリゲート(ラムダ)は、式内の関数ポインタおよびデリゲートの代わりに使用できる。
- ラムダ式にはいくつかの構文があるが、最も短いものは、同等のものが単一の
return
文のみで構成される場合だ。parameter => expression
。 toString()
のより効率的なオーバーロードは、delegate
を引数に取る。