関数テンプレートのオーバーロードにはまる
動作確認環境:VC20052008、VC2010、g++4.5
関数テンプレートの特殊化
任意の型のオブジェクトを出力する次のような関数テンプレートを書くことがありました。
元々が文字列だった場合だけ特別扱いしたい事情があって、テンプレートの特殊化をしています。
// (A) template<class T> void output(T val){ std::cout << val << std::endl; } // (B) template<> void output(const std::string& val) { // 文字列の場合は装飾して表示する std::cout << "***" << val << "***" << std::endl; } // (C) template<> void output(const char* val) { // 文字列の場合は装飾して表示する std::cout << "***" << val << "***" << std::endl; } int main(){ const char* szStr = "Fizz"; output(szStr); // (C) char ary[] = {'B', 'u', 'z', 'z', 0}; char* szStr2 = ary; output(szStr2); // (A) std::string str("foo"); output(str); // (A) const std::string& strRef = str; output(strRef); // これも(A) return 0; }
ただ、これだとうまくいきません。const char* 以外を渡した場合、特殊化バージョンではなくベーステンプレートのほうが呼ばれます。
推測ですが、たとえばstringの場合だと、output()に渡したときには参照ではないstd::string型で渡されていて、ベーステンプレートをstd::stringでインスタンス化したものは完全に型が一致するが、一方特殊化バージョンのほうはconst化と参照へのバインドを行わないといけないので型の一致度が低いので使われない、という事情でしょうか。
言語仕様(C++0x規格書ドラフト:http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2010/n3092.pdf)では「14.8.2 Template argument deduction」あたりに書いてありそうですが、これを追うところまではできませんでした。
関数テンプレートとそのオーバーロード
一般的に、関数テンプレートは特殊化するよりもオーバーロードを使ったほうが良いので(参照:http://www.gotw.ca/publications/mill17.htm)、特殊化をオーバーロードに置き換えてみました。
// (A) template<class T> void output(T val){ std::cout << val << std::endl; } // (D) void output(const std::string& val) { std::cout << "***" << val << "***" << std::endl; } // (E) void output(const char* val) { std::cout << "***" << val << "***" << std::endl; } int main(){ const char* szStr = "Fizz"; output(szStr); // (E) char ary[] = {'B', 'u', 'z', 'z', 0}; char* szStr2 = ary; output(szStr2); // (A)!! std::string str("foo"); output(str); // (D) const std::string& strRef = str; output(strRef); // (D) return 0; }
これだと、std::stringを渡した場合は関数テンプレートではなくてオーバーロードした非テンプレートの関数が呼ばれます。
そもそもどうして関数テンプレートの特殊化ではなく非テンプレート関数によるオーバーロードのほうを使うべきかというと、関数テンプレートよりも非テンプレート関数のほうが引数の型が合っていれば優先して選ばれるので、特定の型の場合だけ別の実装を使いたいという意図通りの結果になるからですし。
しかし、char* を渡した場合にはconst char*を引数に取るオーバーロードが使われずに関数テンプレートのほうが選ばれてしまうのです。
std::stringでは非constのオブジェクトを渡した場合にもconst std::stringを引数に取るオーバーロードが選ばれるのに。
これがなぜなのか分からなくて、困りました。ポインタかどうかあたりで違いがあるのでしょうか。
とりあえず、非constのchar*を取るオーバーロードも追加して一応意図通り動くようにしましたが、気持ち悪い…。
void output(char* val) { output(static_cast<const char*>(val)); }
規格を頑張って読むしかないのかね。
おまけ:g++でtypeinfoから型の名前を得る
ベーステンプレートが実際にどの型でインスタンス化されたかを調べようとtypeinfoを使おうとしたら、g++のtypeinfo::name()はhuman-readableではないマングリングされた名前を返してくることを知りました。
次のように、abi::__cxa_demangle()関数(http://gcc.gnu.org/onlinedocs/libstdc++/latest-doxygen/a01070_source.html#l00145)を使うとdemangleできます。
#include <iostream> #include <typeinfo> #include <cstdlib> #include <cxxabi.h> template<class T> void output(T val){ int status = 0; char* name = abi::__cxa_demangle(typeid(T).name(), 0, 0, &status); std::cout << name << std::endl; std::free(name); std::cout << val << std::endl; } int main(){ const char* szStr = "Fizz"; output(szStr); char ary[] = {'B', 'u', 'z', 'z', 0}; char* szStr2 = ary; output(szStr2); std::string str("foo"); output(str); const std::string& strRef = str; output(strRef); return 0; }
出力:
char const* Fizz char* Buzz std::string foo std::string foo