テンプレートコールバック
doniguruma、作業は順調に遅れております。マジすいません。
今日のネタは、その遅れの原因を作った「コールバックを要求する関数」の作り方です。
例として、「第2、第3引数を第1引数に渡して、その結果を返す関数、call」を作ります。最初は、不肖の初期の設計です。
import tango.io.stdout; int call(int delegate(int,int) f, int i, int j) { return f(i,j); } void main() { Stdout( call( delegate int(int i, int j){ return i+j; }, 3, 4) ).newline; }
これは最も基本的、且つ愚直な記述ですが、極めて大きな問題があります。それは、「第1引数の型が少しでもズレたらダメだ」ということです。
以下に示します。
import tango.io.stdout; int call(int delegate(int,int) f, int i, int j) { return f(i,j); } void main() { // case.1 第1引数の型が「int function(int,int)」型なのでエラー // ~~~~~~~~ Stdout( call( function int(int i, int j){ return i+j; }, 3, 4) ).newline; // case.2 第1引数の型が「int delegate(int,uint)」型なのでエラー // ~~~~ Stdout( call( delegate int(int i, uint j){ return i+j; }, 3, 4) ).newline; // case.3 当然、opCallなどにも対応しない class A { int opCall(int i,int j) { return i+j; } } auto a = new A; Stdout( call(a, 3, 4) ).newline; }
全部のケースでコンパイルエラーになります。特にcase.1は、単にdelegateをfunctionって書いただけなのに、割と面倒なことになってます。かと言って、それぞれのケースに対応したcall関数を作るのはもっと面倒です*1。
では、どうすればいいかというと、「コールバック引数の型をテンプレート化すること」です。
下記のcall関数では、コールバック引数の型をテンプレート化することで、上記で問題の出た3つのケースすべてに対応します。下記ソースのfig.1が、対応箇所です。
import tango.io.stdout; int call(F)( F f, int i, int j) // --fig.1 { return f(i,j); } void main() { Stdout( call( delegate int(int i, int j){ return i+j; }, 3, 4) ).newline; Stdout( call( function int(int i, int j){ return i+j; }, 3, 4) ).newline; Stdout( call( delegate int(int i, uint j){ return i+j; }, 3, 4) ).newline; class A { int opCall(int i,int j) { return i+j; } } auto a = new A; Stdout( call(a, 3, 4) ).newline; }
このやり方なら、多少コールバックの型がズレていても、ちゃんと呼んでくれます。コールバック対象の引数型や戻り値型を厳密にチェックしたいのなら、tango.core.Traitsやstd.traitsを使って、型を解析して静的アサートを取ると良いでしょう。
このやり方はPhobosで割と使われているので、ネタとして知っておくと便利です。でないと、最初のソースのような無様なインターフェースを作る羽目になります orz
*1:ソースが長ければ長いほど、バグを内包する可能性が高くなるからである。