エンジニアのソフトウェア的愛情

または私は如何にして心配するのを止めてプログラムを・愛する・ようになったか

templateの引数に関数を

あまりよい例が思い浮かばなかった。
この例だと効果はたかが知れているのだけれども、もっと面倒くさいことをやっている場面を想像してください。

で。本題は後ろの方なので、ともかく結論を知りたい方は、後ろの方の「やっと本題」以降へどうぞ。


テンプレートの引数に関数は与えられないけれども、関数のアドレスなら与えられるので、実質的にはテンプレートに引数として関数を与えることができますヨ、という話の備忘録。書き方を忘れていてちょっと悩んでしまったので。

最初の例

// 行列を零行列に初期化する、あるいは単位行列に初期化する

#include <iostream>

template<int SIZE>
void initialize_zero(int matrix[SIZE][SIZE])
{
    for(int row = 0; row < SIZE; ++row)
    {
        for(int col = 0; col < SIZE; ++col)
        {
            matrix[row][col] = 0;
        }
    }
}

template<int SIZE>
void initialize_unit(int matrix[SIZE][SIZE])
{
    for(int row = 0; row < SIZE; ++row)
    {
        for(int col = 0; col < SIZE; ++col)
        {
            matrix[row][col] = (row == col) ? 1 : 0;
        }
    }
}

template<int SIZE>
void write(std::ostream& out, int matrix[SIZE][SIZE])
{
    for(int row = 0; row < SIZE; ++row)
    {
        for(int col = 0; col < SIZE; ++col)
        {
            if(col != 0)
            {
                out << ", ";
            }
            out << matrix[row][col];
        }
        out << "\n";
    }
}

int main(int, char* [])
{
    int m[10][10];

    std::cout << "zero matrix\n";
    initialize_zero(m);
    write(std::cout, m);
    std::cout << "\n";

    std::cout << "unit matrix\n";
    initialize_unit(m);
    write(std::cout, m);
    std::cout << "\n";

    return 0;
}


実行結果。

zero matrix
0, 0, 0, 0, 0, 0, 0, 0, 0, 0
0, 0, 0, 0, 0, 0, 0, 0, 0, 0
0, 0, 0, 0, 0, 0, 0, 0, 0, 0
0, 0, 0, 0, 0, 0, 0, 0, 0, 0
0, 0, 0, 0, 0, 0, 0, 0, 0, 0
0, 0, 0, 0, 0, 0, 0, 0, 0, 0
0, 0, 0, 0, 0, 0, 0, 0, 0, 0
0, 0, 0, 0, 0, 0, 0, 0, 0, 0
0, 0, 0, 0, 0, 0, 0, 0, 0, 0
0, 0, 0, 0, 0, 0, 0, 0, 0, 0

unit matrix
1, 0, 0, 0, 0, 0, 0, 0, 0, 0
0, 1, 0, 0, 0, 0, 0, 0, 0, 0
0, 0, 1, 0, 0, 0, 0, 0, 0, 0
0, 0, 0, 1, 0, 0, 0, 0, 0, 0
0, 0, 0, 0, 1, 0, 0, 0, 0, 0
0, 0, 0, 0, 0, 1, 0, 0, 0, 0
0, 0, 0, 0, 0, 0, 1, 0, 0, 0
0, 0, 0, 0, 0, 0, 0, 1, 0, 0
0, 0, 0, 0, 0, 0, 0, 0, 1, 0
0, 0, 0, 0, 0, 0, 0, 0, 0, 1

DRYの原則を

ここで「怠惰」で「短気」で「傲慢」な「よきプログラマ」は「DRYの原則に違反してるっ!」と怒りつつ書き直すことになるわけです。


C言語以来の伝統的なやり方の一つは、関数を受け取る関数にすること。

#include <iostream>

template<int SIZE>
void initialize(int matrix[SIZE][SIZE], int (*generate)(int, int))
{
    for(int row = 0; row < SIZE; ++row)
    {
        for(int col = 0; col < SIZE; ++col)
        {
            matrix[row][col] = generate(row, col);
        }
    }
}

template<int SIZE>
void write(std::ostream& out, int matrix[SIZE][SIZE])
{
    // 変更ないので略
}

int zero(int, int)
{
    return 0;
}

int unit(int row, int col)
{
    return (row == col) ? 1 : 0;
}

int main(int, char* [])
{
    int m[10][10];

    std::cout << "zero matrix\n";
    initialize(m, zero);
    write(std::cout, m);
    std::cout << "\n";

    std::cout << "unit matrix\n";
    initialize(m, unit);
    write(std::cout, m);
    std::cout << "\n";

    return 0;
}


結果は同じなので省略(以下同様)。

高速化を

この例のようなコードでは無理をする必要はないんですが、仮に本当に面倒くさい処理のうえに速度もかせぎたいというとき、テンプレートにしてインライン展開するとかを考えるわけです。

こんな感じで。

#include <iostream>

template<typename GENERATOR, int SIZE>
void initialize(int matrix[SIZE][SIZE])
{
    for(int row = 0; row < SIZE; ++row)
    {
        for(int col = 0; col < SIZE; ++col)
        {
            matrix[row][col] = GENERATOR::generate(row, col);
        }
    }
}

template<int SIZE>
void write(std::ostream& out, int matrix[SIZE][SIZE])
{
    // 変更ないので略
}

struct Zero
{
    inline static int generate(int, int)
    {
        return 0;
    }
};

struct Unit
{
    inline static int generate(int row, int col)
    {
        return (row == col) ? 1 : 0;
    }
};

int main(int, char* [])
{
    int m[10][10];

    std::cout << "zero matrix\n";
    initialize<Zero>(m);
    write(std::cout, m);
    std::cout << "\n";

    std::cout << "unit matrix\n";
    initialize<Unit>(m);
    write(std::cout, m);
    std::cout << "\n";

    return 0;
}


形式と名前さえ一致すれば(ここではgenerateという名前で引数にintを2つとるスタティックなメンバ関数であれば)どんな関数でも呼べる、という利点をみることもできるけれども、余計なコードが多い感じ。

templateの引数に関数を

で。やっと本題。

それなら、テンプレートに関数をわたしてしまおう、というもの。
このばあいのテンプレート引数の書き方を忘れて四苦八苦したので。未来の自分のためにエントリ。

#include <iostream>

template<int GENERATE(int, int), int SIZE> // 第一引数には関数(のアドレス)をとる
void initialize(int matrix[SIZE][SIZE])
{
    for(int row = 0; row < SIZE; ++row)
    {
        for(int col = 0; col < SIZE; ++col)
        {
            matrix[row][col] = GENERATE(row, col);
        }
    }
}

template<int SIZE>
void write(std::ostream& out, int matrix[SIZE][SIZE])
{
    // 変更ないので略
}

inline int zero(int, int)
{
    return 0;
}

inline int unit(int row, int col)
{
    return (row == col) ? 1 : 0;
}

int main(int, char* [])
{
    int m[10][10];

    std::cout << "zero matrix\n";
    initialize<zero>(m);
    write(std::cout, m);
    std::cout << "\n";

    std::cout << "unit matrix\n";
    initialize<unit>(m);
    write(std::cout, m);
    std::cout << "\n";

    return 0;
}

本来、関数のアドレスをわたしているのでinitialize<&zero>(m)と書くのが正当なのだろうと思います。が、アンパサンドを付けなくても関数名単独で書くと値はその関数のアドレスとして扱われるので、「テンプレート引数に関数をわたしている」ように見える書き方ができるわけです。