C++編【言語解説】 第9章 関数テンプレートとインライン関数

この章の概要

この章の概要です。

関数テンプレート

関数には引数があり、引数は特定の型で表現されています。 そのため、型が一致しなければ、その関数を呼び出すことができません。
第8章で説明した、関数オーバーロードを使えば、異なる型を持った同名の関数を定義することができますが、 その場合、型の種類分だけ関数を作らなければならないですし、 何より、未知の型(将来作成するかも知れない構造体など)には対応できません。

そこで登場するのが、関数テンプレートという機能です。 この機能を使うと、型を特定することなく、1つの関数を複数の型で使いまわすことが可能になります。 試しに、前章の関数オーバーロードで使った例を、関数テンプレートを使って書き換えてみます。

#include <iostream>

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

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

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

実行結果:

100
3.5
xyz

関数テンプレートは、通常の関数同様、プロトタイプ宣言と定義を記述できます。 このとき、「template <typename T>」のような表記を先頭に付けることで、関数テンプレートであることを表します。
template」の部分は常にこのように書きますが、< と > の間の記述は変化します。 < と > の間に記述される内容は、テンプレートパラメータ(またはテンプレート仮引数)と呼ばれる指定です。 「<typename T>」と書くことで、T というテンプレートパラメータを定義しています。 なお、この場面においては、typename の代わりに class と書いても同じ意味になります

テンプレートパラメータの指定で classキーワードを使うのは、 C++ が規格化される以前は typenameキーワードが使えない環境が多かったことの名残です。 今でも classキーワードは使えますが、意味合いとしては typename の方が適切だと思われます。

T という名前は慣習的なものであって、別の名前を付けても構いません。 この名前は、関数テンプレートの引数や戻り値、関数の中身で使うことができます。

実際にこの関数を呼び出す際に、T の部分に何らかの型名が当てはめられます。 main関数の中で、

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

このように呼び出していますが、関数名の直後にある < と > で挟まれた型名が、テンプレートパラメータ T のところに当てはめられる訳です。 ここで指定する型は、テンプレート引数(または、テンプレート実引数)と呼ばれます。
今後、< と > の部分の用語は、関数の宣言側がテンプレートパラメータで、呼び出し時に指定するものがテンプレート引数で統一します。

テンプレート引数として int型を指定した、1つ目の呼び出しであれば、

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

こういう write関数と同じになります。 実際、コンパイルの結果、こういう関数が生成されています。 関数テンプレートから、実際の型を当てはめた実際の関数が生成されることを、テンプレートの実体化と呼びます。

このように、関数の宣言と定義の段階では型を具体化せず、呼び出しの際に具体的な型を当てはめます。 そのため、関数内で行う処理が同一であれば、たった1つの関数を作っておくだけで、無限の型に対応できます。 ただし、テンプレート引数が違う呼び出しが複数あると、 コンパイルによって、それぞれの型ごとに別の関数として実体化されますから、 プログラムサイズは大きくなりやすいという欠点もあります。

ところで、テンプレート引数に構造体を指定した場合、引数を渡す際のコピー処理の負荷が気になるところです。 こういう場合、C言語ではポインタを利用しましたが、C++ では参照を利用できます。 参照については、第16章で解説します。


ところで、先ほどのサンプルプログラムでは、関数呼び出しの際に、テンプレート引数を明示的に指定していますが、 実は、実引数から自動的にテンプレート引数を推測してくれます。 そのため、次のように書いても問題ありません。

write(100);
write(3.5);
write("xyz");

実際、このように省略するのが基本ですが、推測だけでは曖昧になってしまう場合には明示的に書くようにします。 曖昧さには、実引数の方をキャストすることでも対処できますが、キャストよりもテンプレート引数を明示する方が適切です。


テンプレートパラメータは複数定義できます。

#include <iostream>

template <typename T1, typename T2>
void write_max(T1 a, T2 b);

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

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

実行結果:

10.5
10.5
10.5

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

write_max関数を呼び出す際、明示的にテンプレート引数を指定することもできますし、実引数から推測させることもできます。 当然と言えば当然ですが、テンプレート引数はテンプレートパラメータの並びと同じ順番で指定します。

また、3つ目の呼び出し例のように、1つ目の引数だけ明示的にテンプレート引数を指定し、2つ目は推測に任せることも可能です。 ご想像の通り、推測に任せられるのは、後ろの方のテンプレート引数だけです。 例えば、1つ目の引数を推測に任せ、2つ目を明示的に指定するということはできません。


戻り値の型にテンプレートパラメータを使う場合は、 実引数からの推測ができませんから、明示的にテンプレート引数を指定する必要があります

#include <iostream>

template <typename RET, typename T1, typename T2>
RET add(T1 a, T2 b);

int main()
{
	std::cout << add<int>(5, 5.2) << std::endl;
	std::cout << add<double>(5, 5.2) << std::endl;
}

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

実行結果:

10
10.2

テンプレートパラメータを定義する際に、戻り値に使うパラメータを先頭に持ってくることがコツで、 こうすることで、呼び出しの際に、戻り値の型だけを明示的に指定し、残りを実引数からの推測に任せることができます

C++11 (戻り値を後ろに置く構文)

先ほどの add関数の例では、2つのテンプレート引数を加算した結果で戻り値の型が決まりますが、 それが何型になるのか事前に決めることができません。 そのため、戻り値の型を明示しなければなりませんが、あまり便利とは言い難いところです。

C++11 には decltype(第7章参照)があるため、式から型を得ることが可能です。 これを利用して、例えば、次のように書けないでしょうか?

template <typename T1, typename T2>
decltype(a + b) add(T1 a, T2 b);

残念ながらこれはコンパイルできません。 問題なのは、decltype(a + b) と書いた時点では、まだ a と b は宣言されていないため、不明な名前だからです。 つまり、戻り値の型を記述する位置は、引数の宣言よりも手前側にあるので、まだ見えていないのです。 これは、C言語から引き継いできた関数宣言の構文の問題と言えます。
そこで、C++11 では、新たな関数宣言の構文が追加されました。 以下のように書くことができます。

template <typename T1, typename T2>
auto add(T1 a, T2 b) -> decltype(a + b);

戻り値の型を auto にすると、引数リストの後ろに -> とともに戻り値の型を記述できるようになります。 戻り値の型を後ろで書くようにすれば、引数に使った名前が見えるようになるという、単純な発想です。
これで、次のようにプログラムを書くことができます。

#include <iostream>

template <typename T1, typename T2>
auto add(T1 a, T2 b) -> decltype(a + b);

int main()
{
	std::cout << add(5, 5.2) << std::endl;
}

template <typename T1, typename T2>
auto add(T1 a, T2 b) -> decltype(a + b)
{
	return a + b;
}

実行結果:

10.2

この構文は、VisualC++ 2013/2015/2017、clang 3.7 のいずれも対応しています

C++14 (戻り値の型推論)

C++14 では、関数の戻り値の型は、return文の内容から自動的に推測させることができます。 この機能により、上述の C++11 の例は、次のように書き換えられます。

#include <iostream>

template <typename T1, typename T2>
auto add(T1 a, T2 b)
{
	return a + b;  // a + b の結果の型に応じて、add の戻り値型が決まる
}

int main()
{
	std::cout << add(5, 5.2) << std::endl;
}

実行結果:

10.2

戻り値の型の指定は auto としておくことは同じですが、関数宣言の後ろに戻り値の型を明示しません (関数宣言の後ろに戻り値の型を置く構文が使えなくなった訳ではありません)。
この構文によって、戻り値の型は、関数本体に含まれる return文の内容から判断させるようになります。 return文がない場合は、void型になります
なお、関数本体に、型が異なる return文が複数存在する場合は、推論に失敗してエラーになります

C++11 のサンプルプログラムと違い、add関数テンプレートの宣言を無くしていますが、 これは、関数本体の return文から型を推論させるため、宣言だけでは戻り値の型が分からず、 add関数テンプレートを使用している箇所でエラーになってしまうためです。

この機能は、VisualC++ 2013 では使用できず、2015/2017 では使用できます。 clang 3.7 は使用できます

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

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

// main.cpp

#include "sub.h"

int main()
{
	write<int>(100);
	write<double>(3.5);
	write<const char*>("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
#define SUB_H


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


#endif

このプログラムは、VisualC++ 2013/2015/2017、clang 3.7 のいずれでも、リンクに失敗します。 例えば、VisualC++ 2015 の場合、次のようなエラーメッセージが出力されます。

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 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 をコンパイルするときには、テンプレートパラメータ T が具体的にどんな型になるのか不明であることに原因があります。 具体的な型は、関数テンプレートを使用している main.cpp の方にありますが、コンパイルという過程では互いの情報を参照することができません。 一応、文法上の問題がある訳ではないので、main.cpp も sub.cpp もコンパイルは成功しますが、 結局、その後のリンクの過程でエラーになってしまいます。

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

この問題を解決するには、関数テンプレートの定義もヘッダファイル側に記述することです。 そうすれば、main.cpp をコンパイルするときに、sub.h に記述された関数テンプレートの定義を参照できるので、うまくコンパイルできます。 また、sub.cpp はそもそも不要になります。
具体的には、以下のようになります。

// main.cpp

#include "sub.h"

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

#ifndef SUB_H
#define SUB_H

#include <iostream>

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


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

#endif

実行結果:

100
3.5
xyz

これで問題は無くなりましたが、 関数テンプレートの宣言と定義は、1つにまとめて次のように書いても構いません。

// sub.h

#ifndef SUB_H
#define SUB_H

#include <iostream>

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

#endif

関数の定義は宣言を兼ねるので、この方が楽ですし、すっきりします。

オーバーロード

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

どれを呼び出すのかを決めるルールは複雑ですが、基本的な考え方としては、 一番一致度の高いものが呼び出されるということです。 ですから、関数テンプレートよりも、通常の関数の方が優先されます

#include <iostream>

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

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

int main()
{
	func("abc");  // T よりも const char* の方が一致度が高い
	func(0);      // 0 は int型なので、const char* よりも T の方が一致度が高い
}

実行結果:

const char*
T

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

この機能は、VisualC++ 2013/2015/2017、clang 3.7 のいずれでも使用できます。

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 が呼び出されて、再帰が完了します。


この機能は、VisualC++ 2013/2015/2017、clang 3.7 のいずれでも使用できます。

インライン関数

関数テンプレートを使えば、内容が同一であれば、型の違いを吸収して関数化できます。 これは、C言語の関数形式マクロ(C言語編第28章)とある面では似ています。
しかし、関数形式マクロを C++ で使うことは勿論可能ですが、できるだけ使うことを避けるべきです。 その代替手段となるのが、関数テンプレートと、この項で説明するインライン関数です。

関数形式マクロの問題は幾つかあります。

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

関数テンプレートを使えばこういった問題が解決されます。 一方、関数形式マクロの利点もあります。

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

1つ目の利点は、インライン関数を使うことで代替可能です。 インライン関数とは、コンパイル時に、その関数の中身が呼び出し元のコードに展開される関数です。 展開されることを指して、インライン展開とかインライン化などと呼ばれます。
関数宣言時に、inlineキーワードを付加することで、その関数はインライン関数になります。 これは、関数テンプレートにでも付加できるので、組み合わせて使うことによって、関数形式マクロを安全に代替できます。
ただし、本当にインライン展開が行われるかどうかは、コンパイラの裁量に委ねられています。 インライン展開に向かないと判断された場合、inlineキーワードは無視され、通常の関数のように扱われます。

2つ目の利点については、マクロを使うことの唯一の利点です。 こればかりは、C++ の機能でも代替できませんが、そもそもこういう手に頼ることはできるだけ避けた方が良いです。


では実際に使ってみます。 ここでは、void*型でないポインタ型を、別のポインタ型へキャストするためのインライン関数を作ってみます。 第7章で取り上げているように、本来このようなキャストには、static_cast を2回行うという手を使いますが、 この面倒さを解消しようという試みです。

#include <iostream>

template <class T>
inline T pointer_cast(void* p)
{
	return static_cast<T>( p );
}

int main()
{
	int a = 10;
	double* b = pointer_cast<double*>(&a);
	int* c = pointer_cast<int*>(b);

	std::cout << *c << std::endl;
}

実行結果:

10

pointer_cast の引数は void*型なので、任意のポインタ型をそのまま渡せます。 従って、1回分の static_cast は不要になります。
あとは、渡されたポインタを、戻り値の型に static_cast で変換して return すれば、 異なるポインタ型間でのキャストが完成します。

戻り値の型がキャスト後の型になる訳ですが、これはテンプレート引数で指定しています。 使い方をよく見ると、static_cast等の C++ で導入されたキャストとまったく同じ構文になっていることが分かります。 使い方を似せることができるので、違和感無く使えます。

なお、複数のファイルから構成されるプログラムを書く場合に、 関数テンプレートのときと同じで、インライン関数の定義はヘッダファイル側に記述する必要があります。 これは、インライン展開を行うためには、そのインライン関数を呼び出している箇所から、関数の中身が見えないといけないためです。


練習問題

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

#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);
}

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

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

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


解答ページはこちら

参考リンク

更新履歴

'2017/3/25 VisualC++ 2017 に対応。

'2016/10/15 clang の対応バージョンを 3.7 に更新。

'2016/1/10 「C++14 (戻り値の型推論)」の項を追加。

'2015/11/15 「C++11 (可変個テンプレートパラメータ)」の説明文を修正。

'2015/10/12 clang の対応バージョンを 3.4 に更新。

'2015/9/5 VisualC++ 2012 の対応終了。

'2015/8/18 VisualC++2010 の対応終了。

'2015/8/15 VisualC++ 2015 に対応。

'2014/10/18 clang 3.2 に対応。

'2014/3/1 新規作成。



前の章へ

次の章へ

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

Programming Place Plus のトップページへ