オブジェクト指向ではポリモーフィズムを活用することで再利用性を高めているということをすでにお話しましたが、C++はオブジェクト指向だけの言語ではありません。オブジェクト指向しかできないほかの言語とは異なり、便利なものは何でも取り入れようという非常に貪欲な設計方針で作られています。こうしたいわば節操の無いやりかたに対して、たびたび他のコミュニティから非難されてきました。
しかし90年代初頭にSTLの原型が現れて、テンプレートへの評価は逆転したと言えるでしょう。JavaやC#には当初はテンプレートがありませんでしたが、genericsという形で移植されてしまいました。それほどこのC++のSTLは衝撃的なものだったのです。
テンプレートのようなプログラミングスタイルはジェネリックプログラミング(総称)と呼ばれ、古くはマクロという形で実現されていたものです。マクロによるプログラミングは複雑に入り組み、読みづらくなり、安全性に疑問が残ります。簡単に言ってしまえば、テンプレートは型安全(タイプセーフ)を考慮したマクロです。そして、全く無関係であると思われていた2つの概念、継承と総称がテンプレートによって1つにまとまりました。
今回はテンプレートのメリットや活用方法についての説明ではなく、テンプレートの仕組みについてのお話です。
#define max(a, b) (((a) > (b)) ? (a) : (b))
この伝統的なマクロにはいくつかの問題があります。max(a++, b) は aが2回インクリメントされてしまいますし、max(foo(), bar()) もまた然りです。またこうしたマクロを自作する際には、カッコの付け忘れに注意しなければなりません。これらの問題はテンプレートを使用することで解決できます(下)。
template const T& max (const T& a, const T& b) { return a > b ? a : b; }
どうでしょうか。何を意味しているのかわかりますか?
テンプレートは一般に理解しにくいと言われています。一部の人は「またC++は複雑な機能を入れた」と言います。しかしテンプレートは実は驚くほど単純な仕組みによって実装されているのです。そしてそれがわかれば、どんな複雑に入り組んだテンプレートでも(簡単にとは言いませんが)理解できるはずです。
では実際にテンプレートがどのように実装されているのか見てみましょう。
test1.cpp
CData &foo()
{
CData left, right;
return max<CData>(left, right);
}
test2.cpp
CMode &bar()
{
CMode left, right;
return max<CMode>(left, right);
}
上記の2つのソースは、どちらもテンプレート関数 max を使用しています。
max 関数が使用しているクラスTはまだ型が定まっていません。test1.cpp では CData になりますし、test2.cpp では CMode になってしまいます。それではいつの時点で、型が定まるのでしょうか。
test1.cpp
CData &foo()
{
CData left, right;
return max(left, right);
}const CData& max(const CData& a, const CData& b)
{
return a > b ? a : b;
}
test2.cpp
CMode &bar()
{
CMode left, right;
return max(left, right);
}const CMode& max (const CMode& a, const CMode& b)
{
return a > b ? a : b;
}
上記のソースはテンプレートがコンパイラによって展開された直後のものです。赤い行は、コンパイラがテンプレートを元にして作成した関数(のイメージ)です。T の部分が置き換わっていることがわかると思います。
このようにコンパイラは、テンプレートを利用している部分があると、そのテンプレートを元にして実際の関数を作り出してしまうのです。
同じ関数があちこちにできてしまうこともあり、コード効率は非常に悪くなります。テンプレートを使用するとコードサイズが激増するので要注意です。このために組込み系ではテンプレートをほとんど使用できません(テンプレートの実体部分を一まとめにする機能を備えたコンパイラも存在します)。
実際にはインライン化される場合がほとんどでしょう。
template <int N> int sum()
{
int s = 0;
for (int i = 0; i < N; i++)
s += i;
return s;
}
void foo()
{
sum<16>() + sum<17>();
}int sum<16>()
{
int s = 0;
for (int i = 0; i < 16; i++)
s += i;
return s;
}int sum<17>()
{
int s = 0;
for (int i = 0; i < 17; i++)
s += i;
return s;
}
テンプレートの<>の部分を、テンプレート引数と呼びます。テンプレートの正体は、テンプレート引数を置き換えるだけの仕組みです。従って上記のようなコードも書けます。
赤い行は例によって、コンパイラが template のコードを元に自動作成したコードのイメージです。ここでは N の部分が置き換わっています。テンプレート引数の部分が置換されただけの同じ関数が2つできています。
このようにテンプレート引数には class 以外のものも指定できるのですが、コード効率が悪くなるため、よほどの理由が無い限りこうした使い方はされません。
こうして仕組みがわかれば、奇妙に見えたテンプレートも理解できるのではないかと思います。
今回の記事ではテンプレートの効能について説明しませんでしたが、テンプレートが使えるのはC++の大きな魅力です。JavaやC#で使えるようになってしまったためにもはや専売特許ではなくなってしまいましたが、それでもテンプレートを利用したプログラミングはC++の大きなアドバンテージだと言えるでしょう。テンプレートを活用しないならC++の魅力は半減ですよ。