こんにちは。田原です。
前回クラス・テンプレートの基礎を解説しました。今回はもう一つのテンプレート、関数テンプレートの基本について解説します。クラス・テンプレートも同様ですが基本的な使い方は簡単です。そして、テンプレートは簡単なものから非常に高度なものまで様々なレベルの機能があります。全てをマスターするのはかなりたいへんです。実は私も全てをマスターしているとまでは言えません。でも、その一部でも使いこなせるようになるとプログラミングの世界が大きく広がります。一緒に頑張りましょう!!
1.関数テンプレートの基本
今回解説するテンプレートは以下の関数テンプレートです。
関数型 | グローバル関数 | 関数テンプレート |
メンバ関数 | メンバ・テンプレート | |
クラス型 | クラス(他のクラスに含まれないクラス) | クラス・テンプレート |
他のクラスの中で定義されたクラス | メンバ・テンプレート | |
その他 | typedef(usingキーワードを使う) | エイリアス(別名)テンプレート |
1-1.まずはサンプルのマクロから
前回もマクロと比較しましたが、今回もマクロと比べてみます。
テンプレートの解説では、よくMAXマクロと比較して説明されることが多いですが、私もその例に倣います。
#include <iostream> #define MAX(a, b) (((a) >= (b))?(a):(b)) int main() { std::cout << MAX(11, 22) << "\n"; }
MAX(a, b)マクロはパラメータaとbを比較して大きい方の値を返却します。
1-1-1.ちょっとうざいカッコ
マクロの場合は特に カッコ()をしつこい位書いておくと安心です。
普通の関数のパラメータは関数に渡される時に計算されるため、パラメータは1つの変数として扱えるのに対し、マクロは文字列を展開しているだけなので、aに 1+1が渡されるとそれは2ではなく、1+1に展開されるのです。上記例の MAX(11, 22)は、(((11) >= (22))?(11):(22)) と展開されます。
何が起こるか見るため、カッコ()を省略してほとんど書かない BAD_MAX()を定義してみました。
#define BAD_MAX(a, b) (a >= b?a:b)
(ちょっと無理やりっぽいですが)例えばBAD_MAX(11 & 0xff, 22 & 0xff)
を処理する場合、
(11 & 0xff >=22 & 0xff?(11 & 0xff:22 & 0xff)
と展開されます。 優先順位は 「>=」>「&」>「?:」の順で高いので、
(11 & (0xff >= 22) & 0xff)?(((11 & 0xff):(22 & 0xff))
のように計算され、結果は想定外の11になります。そのような問題を回避するため、しつこくカッコ()を付けることが推奨されます。
1-1-2.副作用問題
そして、よく指摘されるのですが、マクロは単純に文字列を展開するだけですので、引数に副作用のある式を書くと複数回処理されて意図しない結果になることがあります。
#include <iostream> #define MAX(a, b) (((a) >= (b))?(a):(b)) int main() { int a=10; int b=20; int c=MAX(++a, ++b); std::cout << "a=" << a << " b=" << b << " c=" << c <<"\n"; }
a=11 b=22 c=22
int c=MAX(++a, ++b);
は次のように展開されます。
int c=(((++a) >= (++b))?(++a):(++b);
最初の条件処理で((++a) >= (++b))
が実行され、結果が偽になるので、(++b)が実行された結果、aは+1, bが+2されるからなのです。
関数テンプレートの場合は普通の関数と同じく実引数が渡される時点で処理され、渡された後は通常の変数と同じですから、しつこく()を付ける必要はないですし副作用の適用は1回だけです。
1-2.関数テンプレートの場合
関数テンプレートにする時、様々なやり方がありますが、まずは基本です。
template<typename tType> tType max(tType a, tType b) { return (a >= b)?a:b; }
まず、基本の使い方は、クラス・テンプレートの時と同じく、<>
で括って テンプレート仮引数tTypeに実引数を指定します。以下は int型を指定して暗黙的に実体化した例です。
std::cout << max<int>(11, 22) << "\n";
しかし、関数テンプレートの場合、この「基本の使い方」をすることは少ないです。
多くの場合、次の型推論を一緒に用います。
1-3.型推論
マクロでは型名を指定する必要がないのにテンプレートでは指定が必要ってちょっと面倒ですよね。
しかし、関数テンプレートの場合、型推論の機能を使うことで、型を指定しないことも可能です。
std::cout << max(11, 22) << "\n";
上記の場合、実引数の型はどちらともint型です。(サフィックスを付けて型を明示しない場合、整数リテラルの型はint型と決められています。)
つまり、T a, T b
のaとbにint型が指定されたので、Tはint型でしょうとコンパイラが推論するのです。
このルールもC++標準規格で決められています。constなどの型修飾子や参照、ポインタ、配列が絡むと少し複雑ですが、原則として引数の型となります。型推論については必要に応じて解説します。今は「原則として引数の型となる」と理解下さい。
関数テンプレートでは使い勝手的に型推論に任せることが多いです。
しかし、例えば、aとbに異なる型の引数を渡したい場合、型推論に失敗します。そのような時は型を指定すると良いです。
#include <iostream> template<typename tType> tType max(tType a, tType b) { return (a >= b)?a:b; } int main() { std::cout << " max(11, 22) = " << max(11, 22) << "\n"; // std::cout << " max(11, 22.22) = " << max(11, 22.22) << "\n"; std::cout << "max<double>(11, 22.22) = " << max<double>(11, 22.22) << "\n"; std::cout << " max<int>(11, 22.22) = " << max<int>(11, 22.22) << "\n"; }
クラス・テンプレートの型推論
C++14まではクラス・テンプレートでは型推論を使えませんでした。しかし、C++17で使えるようになります。
2.関数テンプレートのオーバーロード
関数をオーバーロード(多重定義)することができました。
それと同じく、関数テンプレートもオーバーロードできます。
引数リストで呼び分けることができれば、同じ名前の関数と関数テンプレートを定義できるのです。
2-1.便利ツール
テンプレート開発している時、実際に型推論された結果がどのような型になったのか知りたいことが多々あります。そのような時、typeid(型).name()により、その型の名前を得ることができます。ただし、処理系によっては人が見慣れないコンピュータ処理用に「マングル」された名前が返るものがあります。例えばgccです。gccはそれを見慣れた型名に変換する__cxa_demangle()関数を提供しています。
これらを用いて、型を与えたら型名をstd::stringで返却する関数を用意しました。#includeするだけで使えますので、ご自由に使って頂いて良いです。(ただし、
私からの保証はありません
ので、ご自身の責任でお願いします。)
#include <string> #if defined(__GNUC__) #include <cxxabi.h> std::string getNameByTypeInfo(std::type_info const& iTypeInfo) { char* aName; int status = 0; aName = abi::__cxa_demangle(iTypeInfo.name(), 0, 0, &status); std::string ret(aName); std::free(aName); return ret; } #else std::string getNameByTypeInfo(std::type_info const& iTypeInfo) { return iTypeInfo.name(); } #endif #define TYPENAME(dType) getNameByTypeInfo(typeid(dType))
マングルについて
マングルはなるべく短い文字列で型を判別できるようにするための仕組みです。
関数のオーバーロードを実現する際、内部的な関数名は、通常の関数名に加えて引数の各型の名前を繋げたものが使われます。(これにより、適切な関数を呼び出せるのです。)その時、なるべく短い名前で済ませた方がメモリも節約できるしリンク処理も高速化できるのでマングルされた名前が用いられます。
さて、次のコードはその使用例です。先程の関数テンプレートmax()の内部でテンプレート引数の型を表示するようにしてみました。
#include <iostream> #include "typename.h" template<typename tType> tType max(tType a, tType b) { std::cout << "[0] " << TYPENAME(tType) << " max(" << TYPENAME(tType) << " " << a << ", " << TYPENAME(tType) << " " << b << ") -> "; return (a >= b)?a:b; } int main() { std::cout << max(11, 22) << "\n"; std::cout << max(123.4, 567.8) << "\n"; std::cout << max("abcde", "fgh") << "\n"; }
この実行結果は、以下のようになります。うまいこと、型名が表示されていることが分かると思います。
[0] int max(int 11, int 22) -> 22 [0] double max(double 123.4, double 567.8) -> 567.8 [0] char const * max(char const * abcde, char const * fgh) -> abcde
[0] int max(int 11, int 22) -> 22 [0] double max(double 123.4, double 567.8) -> 567.8 [0] char const* max(char const* abcde, char const* fgh) -> abcde
2-2.オーバーロードしてみる
さて、先程の結果を良く見て下さい。Visual C++とgccで同じ結果がでていますが、これはたまたまです。
処理系が異なれば結果も変わります。
- typeid().name()が返却する名前が必ずしも同じになるわけではありません。
実際、64ビットのポインタの名前は両者で異なります。見た目には理解できるのでデバッグ用途には問題はないです。 -
max(“abcde”, “fgh”)の結果
これは、char型へのポインタであるaとbの大きさを較べて大きい方、つまりメモリ上のアドレスで後ろに有る方を返却します。
VC++とgccは、どちらとも先に渡した方の文字列のアドレスが後ろに有ったので、どちらも”abcde”が表示されました。しかし、これはたまたまです。常にそうなる保証はありません。
そもそも文字列の「大きさ」を比較した場合、気持ち的にはアドレスではなく、辞書順で後ろに有る方が返ってきて欲しいですよね。つまり、このmax関数は「バグ」を引き起こしやすく好ましくありません。
そこで、次の関数を追加して、オーバーロードします。
char const* max(char const* a, char const* b) { std::cout << "[1] " << "char const* max(char const* a, char const* b) -> "; return (std::string(a) >= std::string(b))?a:b; }
結果は次のようになります。そして、これは処理系依存しておらず、直感的な動作とも一致します。
[0] int max(int 11, int 22) -> 22 [0] double max(double 123.4, double 567.8) -> 567.8 [1] char const* max(char const* a, char const* b) -> fgh
テンプレートと通常の関数では、通常の関数が優先されると決まっていますので、「曖昧」にならず期待通りに動作します。
もちろん、更にmaxという名前の普通の関数や関数テンプレートを追加することもできます。その時、「曖昧」にならないようにパラメータ設計を慎重に行う必要はありますが、適切に作ればより高度なことができます。
3.おまけ(「テンプレート」の混乱し易いポイント)
ところで、「テンプレート・プログラミング」という概念はちょっと混乱しやすいので、少し補足です。
物を買った時、お金と引き換えに「領収書」を受け取ることもあると思います。
商店の店主さんは未記入の領収書綴りを文房具店などで買うこともあるでしょう。
その個人商店の店主さんが「その未記入の領収書の綴りを買う」時、記入済みの領収証を受け取ることがあると思います。ちょっとややこしいですね。「テンプレート・プログラミング」のややこしさも実はこれと同じです。
- 「テンプレートを記述すること」はこの「領収書の綴りを作ること」と同じです。例えば「領収書の書式を作りプリンタで多数印刷していつでも使えるようにすること」≒「テンプレート・プログラミング」です。
- そして、その未記入の領収書に金額・宛先・但し書き等を記入して判子を押すことが「テンプレートの実体化」に当たります。
- 更に、お客様からの支払いに対してその記入済み(もしかすると収入印紙も貼っているかも)の領収書を渡すことが、関数テンプレートの場合「実体化された関数の呼び出し」、クラス・テンプレートの場合、「実体化されたインスタンスの生成と使用」に当たります。
通常のプログラミングでは、2.の段階がありません。1.プログラミングしたソース・コードを3.直接呼び出して実行します。
そして、テンプレート・プログラミングには2.がありますが、暗黙的実体化により2が暗黙的に実施されるため見えないのです。
先の領収書の例文は記入済みなどと書きましたが、普通は「領収書の綴りを買って領収書を受け取る」と表現すると思います。この後者の「領収書」はどっちの領収書なのでしょう? 記入済み(実体化済み)であることを明示しないと区別が付きませんね。
テンプレートの時も1つ1つ実体化していることを明示すれば混乱し辛いのですが、使う人が一々実体化するのも面倒ですので自動化できる時はコンパイラが自動的に実体化してくれます。便利なのですが実体化が目に見えないため混乱しやすいです。ご注意下さいね。
4.まとめ
今回は、関数テンプレートの基本的な使い方とオーバーロード、そして、テンプレート自体の混乱し易い点の注意事項について解説しました。まだ基礎ですが、すこしずつ難しくなってきてます。私も改めて勉強してます。お互いに頑張りましょう。
さて、次回は明示的実体化について解説したいと思います。使うことは比較的少ない機能ですが、ライブラリを開発する際には把握しておくと好都合な場面もあります。お楽しみに。