関数テンプレート | Programming Place Plus C++編【言語解説】 第9章

トップページC++編

C++編で扱っている C++ は 2003年に登場した C++03 という、とても古いバージョンのものです。C++ はその後、C++11 -> C++14 -> C++17 -> C++20 と更新されており、今後も 3年ごとに更新されます。
なかでも C++11 での更新は非常に大きなものであり、これから C++ の学習を始めるのなら、C++11 よりも古いバージョンを対象にするべきではありません。特に事情がないなら、新しい C++ を学んでください。 当サイトでは、C++14 をベースにした新C++編を作成中です。

この章の概要

この章の概要です。


関数テンプレート

第8章で説明した関数オーバーロードを使えば、異なる型を持った同名の関数を作れますが、関数の作成者が想定した範囲内でしか対応できません。しかも、型ごとに1つ1つ関数を定義しなければなりません。何より、関数オーバーロードでは、将来追加されるかもしれない未知の型にまでは対応できません。

そこで登場するのが、関数テンプレートという機能です。この機能を使うと、1つの関数の実装を、複数の型で使いまわすことが可能になります。これは、未知の型であっても対応できる適用力を持っています。

関数テンプレートを使うにはまず、通常の関数のように宣言と定義を記述します。このとき、先頭に template というキーワードを置くことによって、それが通常の関数ではなく、関数テンプレートであることを表します。

// 宣言
template <typename テンプレート仮引数名>
戻り値の型 関数テンプレート名(仮引数の並び);

// 定義
template <typename テンプレート仮引数名>
戻り値の型 関数テンプレート名(仮引数の並び)
{
    本体のコード
}

typenameキーワードは、変わりに classキーワードを使っても構いません。まったく同じ意味です。

< と > の間に記述される内容は、テンプレート仮引数(テンプレートパラメータ)と呼ばれる指定です。「<typename T>」と書いた場合、T という名前のテンプレート仮引数を定義しています。T は慣例的に使われている名前です。もちろん、分かりやすく命名することが理想的です。

テンプレート仮引数は複数あっても構いませんが、最低でも1つは必要です。テンプレート仮引数を複数個使う例は、後で取り上げます

テンプレート仮引数名の手前に typename や class を置くことは、そのテンプレート仮引数が「型」であることを意味しています。型でないテンプレート仮引数もあり得ます(第22章)。

関数テンプレートの定義の中には、通常の関数を実装するのと同じように本体のコードを記述します。本体のコードおよび、仮引数や戻り値のところでは、テンプレート仮引数を使用できます。通常の関数であれば、「int」とか「char」などと記述しますが、その代わりに「T」のような記述が可能になるということです。

関数テンプレートで使われているテンプレート仮引数は、その関数テンプレートを使用するときに与えるテンプレート実引数によって置き換えられます。

試しに、前章の関数オーバーロードで使った write関数の例を、関数テンプレートを使って書き換えてみます。

#include <iostream>
#include <string>

template <typename T>
void write(T a);

int main()
{
    write<int>(100);
    write<double>(3.5);
    write<char>('x');
    write<std::string>("xyz");
}

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

実行結果:

100
3.5
x
xyz

いつものように、宣言と定義はまとめてしまっても構いません。

通常の関数を使用するときに、関数呼び出しを行うのと同様に、関数テンプレートを使用するときにも、関数呼び出しの構文を用います。このとき、テンプレート実引数の指定を行えます。

関数テンプレート名<テンプレート実引数の並び>(実引数の並び)

テンプレート仮引数が複数あるのなら、その順番どおりに「,」で区切って書き並べます。

指定したテンプレート実引数が、テンプレート仮引数のところに当てはめられます。たとえば、1つ目の呼び出しでは「int」が指定されているので、write関数テンプレートのテンプレート仮引数 T は「int」で置き換えられます。同様に2つ目の呼び出しでは、T を「double」に置き換えます。

コンパイラは関数テンプレートの実際の使われ方をみて、テンプレート仮引数をテンプレート実引数で置き換えたコードを生成します。たとえば、次のようになります。

void write(int a)
{
    std::cout << a << std::endl;
}

void write(double a)
{
    std::cout << a << std::endl;
}

この置き換え後のコードは、通常の関数と同じ構文ルールに従っていることが分かります。つまり、関数テンプレートから関数が生み出されているといえます。これは、関数テンプレートの実体化と呼ばれています。また、実体化して生み出された関数を、関数テンプレートの特殊化といいます。

特殊化という用語の意味はかなり分かりづらいと思います。「化」が付いているため、これから起こる何らかの変化のことを言っていそうですが、そうではなく、変化(=実体化)を終えた結果、できあがったもののことを言っています。

実体化は、テンプレート実引数が異なる呼び出しを見つけるたびに行われるため、使われ方のパターンが多いと、それだけプログラムサイズは大きくなります。

複数個のテンプレート仮引数

テンプレート仮引数は複数個あっても構いません。

#include <iostream>

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

int main()
{
    write_max<int, int>(10, 11);
    write_max<double, int>(10.5, 10);
    write_max<int, double>(10, 10.5);
}

実行結果:

11
10.5
10.5

write_max関数は、2つの実引数のうち、大きい方を選んで標準出力へ出力します。このとき、2つの実引数の型が異なることを許すため、2つのテンプレート仮引数を利用しています。もし、1つのテンプレート仮引数だけで実現しようとすると、2つのテンプレート実引数が同じ型でなければならなくなります。

#include <iostream>

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

int main()
{
    write_max<int>(10, 11);
    write_max<double>(10.5, 10);
    write_max<double>(10, 10.5);
    write_max<int>(10, 10.5);
}

実行結果:

11
10.5
10.5
10

実引数の値を、テンプレート実引数の型に暗黙的に変換できる限りは、コンパイルは通りますが、情報を失う恐れはあります。4つ目の呼び出しでは、10.5 という double型の値を、int型に変換しています。


テンプレート実引数の推定

ここまでのサンプルプログラムでは、関数テンプレートの呼び出しの際に、テンプレート実引数を明示的に指定していますが、テンプレート実引数の型を、コンパイラに自動判断させることができます。この機能は、テンプレート実引数の推定と呼ばれます。

テンプレート実引数の推定は、関数テンプレートを呼び出す際に渡す実引数の型を使って行われます。たとえば、write関数テンプレートの例で考えてみます。write関数テンプレートは、次のように宣言されています。

template <typename T>
void write(T a);

テンプレート実引数の推定を使うには、この関数テンプレートを使用するときに、次のように書きます。

write(100);

テンプレート仮引数 T が、仮引数a の型として使われています。そのため、仮引数a の型が決まれば、それがテンプレート仮引数 T の型であると考えられます。そして、仮引数a の型は、実引数の型から判断できます。実引数の「100」は int型なので、仮引数a も int型、テンプレート仮引数 T も int型であると判断できるという流れです。いわば、反対側(使用者の側)から確定させていくような流れになります。

テンプレート実引数の推定は積極的に使ってよい機能です。明示的にテンプレート実引数を指定する方法では、暗黙の型変換が入るせいで、指定間違いを検出できないことがあります。また、コードが非常にシンプルになります。

この章の最初のサンプルプログラムを書き換えてみます。

#include <iostream>

template <typename T>
void write(T a);

int main()
{
    write(100);    // 100 は int なので、T は int
    write(3.5);    // 3.5 は double なので、T は double
    write('x');    // 'x' は char なので、T は char
    write("xyz");  // "xyz" は const char[] なので、T は const char*
}

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

実行結果:

100
3.5
x
xyz

4つ目の呼び出しだけは、最初のサンプルプログラムとは結果が変わっています。文字列リテラルは、const char[] であって、std::string ではありません。また、配列のまま渡せないので、const char* であると扱われます。もし、std::string として扱わせたいのであれば、テンプレート実引数を明示的に指定します。

テンプレート仮引数が複数個ある場合、末尾側だけをテンプレート実引数の推定に任せて、手前側は明示的に指定するという方法を取ることもできます。次のサンプルプログラムで、3つの関数テンプレートの呼び出しは、いずれも同じ意味です。

#include <iostream>

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

int main()
{
    write_max(10, 10.5);
    write_max<int>(10, 10.5);
    write_max<int, double>(10, 10.5);
}

実行結果:

10.5
10.5
10.5

ところで、戻り値の型にテンプレート仮引数を使う場合は、関数の実引数からは推測ができません。あまり考えずに実装すると、次のように明示的にテンプレート実引数を指定しなければならなくなり、不便です。

#include <iostream>

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

int main()
{
    // T1、T2、RET の型をそれぞれ明示的に指定せざるを得ない
    double result = add<int, double, double>(5, 5.2);
    std::cout << result << std::endl;
}

実行結果:

10.2

こういうケースでは、戻り値の型に使うテンプレート仮引数を1つ目に定義します。テンプレート実引数の推定に任せられない部分を手前側に持ってくれば、そこだけ明示的に指定して、残りは推定させることができるという考え方です。

#include <iostream>

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

int main()
{
    // RET の型だけを明示的に指定し、T1、T2 の型は実引数から推定させる
    std::cout << add<double>(5, 5.2) << std::endl;
}

実行結果:

10.2

テンプレート仮引数と関数の仮引数

関数テンプレートが、関数の仮引数の部分でテンプレート仮引数を使うとき、単純に「f(T a)」のように使う以外に、ほかの要素を組み合わせた使い方ができます。

次の例では、「T」を「T*」として使っています。

#include <iostream>

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

int main()
{
    int v = 100;

    write<int>(&v);
    write(&v);
}

実行結果:

100
100

テンプレート実引数を明示的に指定している方に注目すると、「<int>」となっています。ここが「<int*>」でないことは重要です。「*」は仮引数 p の方に付いていますから、テンプレート仮引数 T に当てはめるべき型は「int」であって、「int*」ではないのです。

テンプレート実引数の推定に任せている方も同じ結果になります。実引数は「&v」なので「int*」です。仮引数 p の型が「T*」なので、「T」に当てはめるべきものは「int」であることを推定してくれます。

write関数テンプレートの仮引数 p を constポインタに変えても、やはり理屈は変わらず、同じ結果になります。

#include <iostream>

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

int main()
{
    int v = 100;

    write<int>(&v);  // OK. T は int、p は const int*
    write(&v);       // OK. T は int、p は const int*
}

実行結果:

100
100

仮引数 p の方を非constポインタに戻して、ローカル変数 v を const にすると、コンパイルエラーになります。

#include <iostream>

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

int main()
{
    const int v = 100;

    write<int>(&v);  // コンパイルエラー
                     // T を int とすると、仮引数 p は int* であり、constポインタを渡せない
    write(&v);       // OK。T は const int、p は const int*
}

これは明示的に指定したテンプレート実引数に問題があります。「T」を「int」としてしまうと、仮引数 p の型は「int*」になりますから、「const int*」である「&v」を渡せません。

一方、テンプレート実引数の推定に任せている方は、「&v」が「const int*」であることを認識できるため、「T」に当てはめるべきものが「const int」であることを判断可能です。「T」が「const int」に置き換えられると、仮引数 p の型は「const int*」になりますから、「&v」を渡せます。

ところで、仮引数が「T*」のようにポインタ型になっていなければ、ポインタを渡せないというわけではないことにも注意が必要です。

#include <iostream>

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

int main()
{
    int v = 100;

    write<int*>(&v);  // OK. T は int*、a は int*
    write(&v);       // OK。T は int*、a は int*
}

実行結果:

006FF9C8
006FF9C8

この場合は、「T」に当てはめられる型が「int*」になるということです。このように、テンプレート仮引数自身に「*」が含まれることもあるし、「T」の方には含めずに、「T*」のように使うこともできます。


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

関数テンプレートをオーバーロードすることは可能です。関数テンプレートと通常の関数とのあいだでオーバーロードすることも可能です。

#include <iostream>

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

template <typename T>
void func(T* p)
{
    std::cout << "T*" << std::endl;
}

void func(const char* s)
{
    std::cout << "const char*" << std::endl;
}


int main()
{
    int v = 100;
    char s[] = "abc";

    func(v);
    func(&v);
    func(s);

    const int cv = 100;
    const char cs[] = "abc";

    func(cv);
    func(&cv);
    func(cs);
}

実行結果:

T
T*
T*
T
T*
const char*

オーバーロードされている関数を呼び出す際に、どの関数が選択されるのかを決定するルールは、すでに前章で説明しています。前章の時点では、関数テンプレートに関する考慮は入っていなかった訳ですが、前章で見たルール自体には変化はありません。このルールの手前に、新たな段階が加わります。

一応、以下に流れを簡略化したものを書いておきますが、詳細を知る必要はまずありません。ルールはプログラマーの感覚にできるだけ沿うように設計されています。もちろん感覚に合わない結果になることはありますが、そのようなときは、明示的に型を指定するなどして、プログラムを工夫した方が良いです。

オーバーロードの中に関数テンプレートが含まれている場合はまず、関数テンプレートの特殊化のパターンを洗い出すことから始めます。つまり、実引数を渡して、呼び出すことが可能なパターンを探し出すということです。

たとえば、2つ目の呼び出し「func(&v)」を考えてみます。「&v」の型は「int*」ですから、仮引数が「T」の関数にも「T*」の関数にも渡せます。そのため、「func<int*>(int)」と「func<int>(int*)」という2つの特殊化が候補に挙がります。

そのあと、候補に挙がった特殊化の中で、もっとも限定的な使い方しかできないものが選択されます。「func(T a)」の方はあらゆる型を渡せますが、「func(T* p)」の方はポインタしか渡せませんから、後者の方が選択されるという訳です。

こうして、関数テンプレートの中の最終候補が決定したら、あとの流れは同じです。最終候補の関数テンプレートと、通常の関数とを合わせて、前章で見たルールで「呼び出し可能な最適関数」を決定します。

「呼び出し可能な最適関数」を決定するときに引数の一致度を調べますが、関数テンプレートに関しては、型が確定済みなので、さらに型を拡張したり、暗黙な型変換を適用したりはしません。

もし、最終候補に挙がった関数テンプレートと通常の関数の中で、引数の一致度が同じになるものがあれば、通常の関数の方が優先されます。

テンプレートの実装を記述する位置

複数のファイルを使ったプログラムでは、関数宣言をヘッダファイルに記述し、定義をソースファイル側に記述しますが、関数テンプレートの場合、これがうまくいきません。

// main.cpp

#include "sub.h"

int main()
{
    write(100);
    write(3.5);
    write('x');
    write("xyz");
}
// sub.cpp

#include <iostream>
#include "sub.h"

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

#ifndef SUB_H_INCLUDED
#define SUB_H_INCLUDED

template <typename T>
void write(T a);

#endif

このプログラムは、Visual Studio、clang のいずれでも、リンクに失敗します。たとえば、Visual Studio は、次のようなエラーメッセージを出力します。

error LNK2019: 未解決の外部シンボル "void __cdecl write<int>(int)" (??$write@H@@YAXH@Z) が関数 _main で参照されました。 C:\main.obj CppTest
error LNK2019: 未解決の外部シンボル "void __cdecl write<double>(double)" (??$write@N@@YAXN@Z) が関数 _main で参照されました。   C:\main.obj CppTest
error LNK2019: 未解決の外部シンボル "void __cdecl write<char>(char)" (??$write@D@@YAXD@Z) が関数 _main で参照されました。   C:\main.obj CppTest
error LNK2019: 未解決の外部シンボル "void __cdecl write<char const *>(char const * const)" (??$write@PBD@@YAXQBD@Z) が関数 _main で参照されました。 C:\main.obj CppTest
error LNK1120: 3 件の未解決の外部参照 C:\Debug\CppTest.exe    CppTest

要するに、関数テンプレートwrite が、main.obj (main.cpp をコンパイルして生成されるオブジェクトファイル) から参照されているが、その定義が見つからないということです。

定義は sub.cpp にあるのになぜ見つからないかというと、sub.cpp にあるのは関数テンプレートの定義であって、それが実体化したものではないからです。実体化は、関数テンプレートを使う側が、テンプレート実引数を指定(あるいは推定)しないと行えません。しかし、実体化しようにも、main.cpp からは sub.cpp の中身は見えませんから、それもできません。

構文的な問題があるわけではないので、main.cpp も sub.cpp もコンパイルは成功しますが、リンクの過程でエラーになってしまいます。

コンパイラによっては、この問題が起こらないものもありますが、それはテンプレートの取り扱い方の方針が異なるためです。しかしそういうコンパイラは少数派であり、ほとんどのコンパイラは同じ問題を抱えています。

この問題を解決するには、関数テンプレートの定義をヘッダファイル側に記述することです。そうすれば、関数テンプレートを使っている main.cpp から、関数テンプレートの定義が見えるので、実体化することが可能です。

具体的には、以下のようになります。sub.cpp にある関数テンプレートの定義を sub.h へ移動します。もはや宣言は不要なので削除しています。

// main.cpp

#include "sub.h"

int main()
{
    write(100);
    write(3.5);
    write('x');
    write("xyz");
}
// sub.h

#ifndef SUB_H_INCLUDED
#define SUB_H_INCLUDED

#include <iostream>

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

#endif

実行結果:

100
3.5
x
xyz

これは関数テンプレートの大きな欠点の1つです。関数の定義がヘッダファイルに露出してしまうため、実装を隠すことができません。また、それが原因で、実装を少しでも変更すると、使用者側のプログラムの再コンパイルが必要になります。


C++11 (デフォルトテンプレート実引数)

C++11

C++11 では、テンプレート仮引数に、デフォルトテンプレート実引数を与えることが可能になりました。

意外と使いどころが難しい新機能なので、良い例とは言い難いですが、次のように書けます。

#include <iostream>

template <typename RET = double>
RET get_value()
{
    return static_cast<RET>(1.5);
}

int main()
{
    std::cout << get_value() << std::endl;
    std::cout << get_value<int>() << std::endl;
}

実行結果:

1.5
1

C++11 (可変個テンプレート仮引数)

C++11

C++11 では、テンプレート仮引数の個数を可変にできるようになりました。

可変個の任意の型の引数を渡して、すべてを標準出力へ出力する write関数を作ってみます。

template <typename... ARGV>
void write(ARGV... argv)
{
    // 省略
}

int main()
{
    write();
    write('a');
    write(10, "xyz", 0.5);
}

テンプレート仮引数に、「…」が使われています。この「…」を、テンプレート仮引数パックと呼び、可変個(0個以上)のテンプレート仮引数があることを意味します。

なお、「typename」は「class」でも構わないですし、「…」の前後にスペースがあっても構いません。

また、関数の仮引数の方にも「…」が使われており、こちらは、関数パラメータパックと呼びます。これも、可変個(0個以上)の引数があることを意味しています。「ARGV…」となっているように、テンプレート仮引数パックに使った名前とセットになっています。

問題は、こうして定義された関数テンプレートwrite の中身をどう記述すれば良いかです。これが意外と難しいのですが、再帰処理を駆使して記述する必要があります。

#include <iostream>

void write_inner()
{
}

template <typename T, typename... ARGV>
void write_inner(T first, ARGV... argv)
{
    std::cout << first << " ";
    write_inner(argv...);
}

template <typename... ARGV>
void write(ARGV... argv)
{
    write_inner(argv...);
    std::cout << std::endl;
}

int main()
{
    write();
    write('a');
    write(10, "xyz", 0.5);
}

実行結果:


a
10 xyz 0.5

呼び出し順に見ていきましょう。

まず、関数テンプレートwrite が呼び出され、その中で、可変個引数 argv を、write_inner へ渡しています。このときにも「…」を付けておく必要があります。この記号があることで、可変個の値をまとめて取り扱っているのだということを明示する訳です。

ポイントとなるのは、write_inner がオーバーロードされていて2種類ある点です。一方は、引数が無く、関数テンプレートにもなっていない普通の関数です。他方は、可変個引数を持った関数テンプレート版です。

関数テンプレートwrite から、どちらが呼び出されるのかというと、argv が実際に何個の値を持っているかによります。これが 0個であれば、引数無し版が呼び出され、1個以上あれば可変個引数版が呼び出されます。

可変個引数版の write_inner は、「…」の付いていない引数 first を持っています。これによって、先頭の1つだけが first として与えられ、残りの可変個部分が argv として与えられます。

こうして、最初に関数テンプレートwrite に渡された可変個引数の先頭部分から1つ引きはがされ、標準出力への出力が行われ、残りの可変個引数は再び write_inner へと引き渡されます。結果、再帰的に write_inner が呼び出されていきますが、いずれ可変個引数部分が 0個になり、引数無し版の write_inner が呼び出されて、再帰が完了します。


練習問題

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

#include <iostream>
#include <string>

struct MyData {
    int value;
    std::string name;
};

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

int main()
{
    MyData a, b;
    a.value = 10;
    a.name = "aaa";
    b.value = 20;
    b.name = "bbb";

    write_max(a, b);
}

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


解答ページはこちら

参考リンク


更新履歴

’2019/2/12 VisualStudio 2015 の対応終了。

’2018/8/22 「インライン関数」の項を、第10章へ移動。同内容を扱っていた練習問題②も移動。

’2018/8/18 全体的に見直し修正。
関数テンプレート」の項の内容を細分化して、「複数個のテンプレート仮引数」「テンプレート実引数の推定」に分けた。
テンプレート仮引数と関数の仮引数」の項を追加。
「C++11 (戻り値を後ろに置く構文)」「C++14 (戻り値の型推論)」の項を削除。

≪さらに古い更新履歴を展開する≫



前の章へ (第8章 関数オーバーロード)

次の章へ (第10章 マクロとその代替)

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

Programming Place Plus のトップページへ



はてなブックマーク に保存 Pocket に保存 Facebook でシェア
X で ポストフォロー LINE で送る noteで書く
rss1.0 取得ボタン RSS 管理者情報 プライバシーポリシー
先頭へ戻る