テンプレートコールバック

 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:ソースが長ければ長いほど、バグを内包する可能性が高くなるからである。