Modern C++編【言語解説】 第11章 関数テンプレート

先頭へ戻る

この章の概要

この章の概要です。

関数テンプレート

第8章で説明したクラステンプレートを使うと、 クラス内で使われる型や定数を、利用者の側で決定することができました。 同じことを関数に対しても行うことができ、これを関数テンプレートと呼びます。 関数テンプレートを使うと、関数が使う型や定数を、テンプレート実引数によって指定することができます。

前章の関数オーバーロードで使った write() の例を、 関数テンプレートを使ったプログラムに置き換えてみます。

#include <iostream>

namespace {

    template <typename T>
    void write(T a)
    {
        std::cout << a << std::endl;
    }
    
}

int main()
{
    write<int>(100);
    write<double>(3.5);
    write<const char*>("xyz");
}

実行結果:

100
3.500000
xyz

関数テンプレートを定義する際の記法は、クラステンプレートと同じです。 「template <typename T>」のような記述を定義の直前に書くことで、テンプレートであることを示し、 テンプレートパラメータを宣言しています。 typename の代わりに class を使って良いことも同様ですし、T が慣習的な名前であることも同様です。

関数テンプレートは、通常の関数と同様に、宣言と定義をそれぞれ記述しても構いません。 その場合、両方ともに、上述のようなテンプレートであることを示す表記が必要です。

// 宣言
template <typename T>
void write(T a);

// 定義
template <typename T>
void write(T a)
{
    std::cout << a << std::endl;
}

関数テンプレートを使う側は、テンプレート実引数を指定する他は、通常の関数呼び出しと同じように使えます。 関数テンプレート名の直後に「<int>」のような記述を置いて、テンプレート実引数を指定しています。

しかし実際には、テンプレート実引数は明示的に指定せずとも、実引数の方で判断できる場合もあります。 write() の引数は T型であり、T はテンプレートパラメータであることがコンパイラには分かっているので、 実引数に「100」のような値を指定すれば、T が int であることを理解してくれます。 このように、コンパイラに自動判断させる場合は、次のように記述します。

write(100);    // 100 は int。よって T は int
write(3.5);    // 3.5 は double。よって T は double
write("xyz");  // "xyz" は const char[]。引数は const char* に変換され、よって T は const char*

最早、通常の関数呼び出しと区別を付ける必要は無くなりました。

実引数から判断できない場合や、「100」を short型として扱って欲しい場合などには、 キャストを使って型を明確にすることもできますが、 明示的にテンプレート実引数を指定するようにした方が良いでしょう。


関数テンプレートは、関数オーバーロードで複数の関数を定義するのと違い、未知の型にも対応できる点が見逃せません。 つまり、関数テンプレートよりも後で作られたクラス型でも、テンプレート実引数に指定することができます。

戻り値の型にテンプレートパラメータを使う

戻り値の型にテンプレートパラメータを使う場合は、 実引数からの推測ができませんから、常にテンプレート実引数を明示的に指定しなければなりません。

もし、テンプレートパラメータが複数ある場合、戻り値の型に使うものを除けば、 テンプレート実引数を明示的に指定しなくて済むかも知れません。 そこで、テンプレートパラメータを宣言する順番を工夫して、戻り値に使うテンプレートパラメータを先に持ってきます。

#include <iostream>

namespace {

    template <typename RET, typename T1, typename T2>
    RET add(T1 a, T2 b)
    {
        return static_cast<RET>(a + b);
    }

}

int main()
{
    int ans1 = add<int>(5, 5.2);
    double ans2 = add<double>(5, 5.2);

    std::cout << ans1 << std::endl;
    std::cout << ans2 << std::endl;
}

実行結果:

10
10.2

テンプレートパラメータは3つあるのに対して、テンプレート実引数の指定は1つだけになっています。 テンプレートパラメータとテンプレート実引数の並び順は対応しているので、 指定されたのは、1つ目のテンプレートパラメータ RET の型で、 残りの2つは、実引数(5 と 5.2)から判断されます。 よって、テンプレートパラメータ T1 は int、T2 は double です。

このように、戻り値の型として使うテンプレートパラメータを先頭に持ってこれば、 それだけを明示的に指定して、残りはコンパイラによる自動判断に任せることができます。 使いやすい関数テンプレートを作るための1つのコツなので、覚えておいて下さい。

関数テンプレートのオーバーロード

関数テンプレートは、オーバーロードすることができます。 これは、関数テンプレート同士でも可能ですし、通常の関数との間でも可能です。

#include <iostream>

namespace {

    bool Equal(const char* s1, const char* s2)
    {
        return std::strcmp(s1, s2) == 0;
    }

    template <typename T>
    bool Equal(T a1, T a2)
    {
        return a1 == a2;
    }

}

int main()
{
    const char s1[] = "abc";
    const char s2[] = "abc";

    std::cout << Equal(s1, s2) << "\n"
              << Equal(123, 123) << std::endl;
}

実行結果:

1
1

関数テンプレートと、通常の関数との間でオーバーロードを行った場合に、どちらが呼び出されるかには注意が必要です。 正確なルールは複雑ですが、ごく単純に言えば、より一致度が高い方が優先されます。 そのため、関数テンプレートと通常の関数との間でオーバーロードを行っていて、 通常の関数の引数と、呼び出し側の実引数とが適合するのなら、通常の関数が優先されます。

このサンプルプログラムの場合、1つ目の呼び出しでは、実引数の型が const char[] なので、 const char* の仮引数を持つ通常の関数の方が、より直接的に一致するとみなされます。 よって、std::strcmp() を使った一致比較が行われて、true が返されます。
2つ目の呼び出しでは、実引数の型が int なので、const char* の仮引数を持つ通常の関数の方は一致しません。 関数テンプレートの Equal() の方になら、int を渡すことが可能なので、こちらが使われます。

1つ目の呼び出しでも、関数テンプレートの Equal() が適合しない訳ではないので、 次のように、テンプレート実引数を明示すれば、呼び出すことが可能です。

int main()
{
    const char s1[] = "abc";
    const char s2[] = "abc";

    std::cout << Equal<const char*>(s1, s2) << "\n"
              << Equal(123, 123) << std::endl;
}

実行結果:

0
1

この場合、文字列比較を ==演算子で行うため、アドレス同士の比較となります。 そのため、false が返されています。

ここでテンプレート実引数を明示的に指定したのは、 コンパイラの自動判断では、プログラムが意図した方の関数を使ってくれないことへの対処のためです。 関数テンプレート版の Equal() しか存在しないのなら、 テンプレート実引数を明示することなく呼び出せます。

ノンタイプテンプレートパラメータ

クラステンプレートの場合と同様、関数テンプレートでもノンタイプテンプレートパラメータを使うことができます。基本的には同じことなので、第8章の解説を参照して下さい。

関数形式マクロの代替

関数テンプレートを使えば、型の違いを吸収して関数化できます。この特徴は、C言語の関数形式マクロ(C言語編第28章)と似ています。

C言語でもそうですが、関数形式マクロの使用は可能であれば避けた方が良いです。 例えば、関数形式マクロには、以下のような問題があります。

  1. 引数の型や個数、戻り値が明確でない。
  2. MACRO(++a); のような、インクリメントやデクリメントを伴う問題を避ける手段が無く、本質的に常に危険である(C言語編第28章
  3. プリプロセスで置換されるので、名前空間に含めることができない。

一方、関数形式マクロには、以下のような利点があります。

  1. 使用箇所にコードが展開されるので、関数呼び出しのコストを避けることができる。
  2. #演算子(C言語編第28章)、##演算子(C言語編第28章)のような特殊な機能が使えるため、関数では表現できない結果を生むことができる。

問題点に関しては、関数テンプレートを使えば解決します。
1つ目の利点に関しては、インライン関数で代替できます。2つ目の利点については、マクロに頼らざるを得ませんが、そもそもあまり濫用して良いものではありません。

このように、関数形式マクロは、inline を指定した関数テンプレートで、より安全に代替できます。


練習問題

問題① 次のプログラムがコンパイルエラーになる理由を答えて下さい。

#include <iostream>
#include <string>

namespace {

    struct MyData {
        int v1;
        int v2;
    };

    template <typename T1, typename T2>
    inline void write_max(T1 a, T2 b)
    {
        std::cout << ((a >= b) ? (a) : (b)) << std::endl;
    }

}

int main()
{
    MyData a, b;
    a.v1 = 10;
    a.v2 = 20;
    b.v1 = 20;
    b.v2 = 0;

    write_max(a, b);
}

問題② 次の関数形式マクロを、より安全な方法で置き換えて下さい。

#define MAX(a,b) ((a) > (b) ? (a) : (b))

問題③ 任意の型の配列から、任意の値を線形探索(アルゴリズムとデータ構造編【探索】第1章)で探す関数テンプレートを作成して下さい。


解答ページはこちら

参考リンク

更新履歴

'2017/8/2 新規作成。





前の章へ(第10章 関数オーバーロードとデフォルト引数)

次の章へ(第12章 参照)

Modern C++編のトップページへ

Programming Place Plus のトップページへ


このエントリーをはてなブックマークに追加
rss1.0 取得ボタン RSS