プログラミング言語C++の文字列リテラル/ヌル終端文字列に対して、配列参照型をとる関数テンプレートf(const char(&)[N])
とポインタ型をとる関数f(const char*)
のオーバーロードは、プログラマの期待通りには振る舞わない。
本記事の内容はStack Overflowで見つけた質問と回答に基づく。
文字列リテラルはconst char
配列型(→id:yohhoy:20150213)のため、配列参照型const char(&)[N]
でうけることで文字列長Nを定数時間 O(1)*1 で取得できる。リテラル以外のヌル終端文字列はポインタ型const char*
で受ける必要があるが、文字列長の計算には線形時間 O(N)*2 を要してしまう。下記コードのように、文字列リテラルへの最適化を目的にオーバーロード関数を提供しても、非効率なポインタ型バージョン#2のみが利用される。このプログラマの期待に反した振る舞いは、C++における関数オーバーロード解決ルールに起因する。
// #1 配列参照型をとる関数テンプレート template <size_t N> void sink(const char (&s)[N]) { std::cout << "array=" << s << " len=" << (N - 1) << std::endl; } // #2 ポインタ型引数をとる関数 void sink(const char *s) { std::cout << "ptr=" << s << " len=" << std::strlen(s) << std::endl; } const char *msg = "Hello"; sink(msg); // #2が呼び出される sink("Hello"); // #2が呼び出される(#1ではない) sink<>("Hello"); // <>を明示すれば#1が呼び出されるが... sink<>(msg); // NG: 文字列リテラル以外でコンパイルエラー
オーバーロード解決の優先順位を制御するため、ポインタ型バージョン#2を関数テンプレートで定義すれば、一応は所望の振る舞いを実装できる。そこまでやる?
// #1 配列参照型をとる関数テンプレート template <size_t N> void sink(const char (&s)[N]); // #2 char const*へ変換可能な型Tをとる関数テンプレート template <typename T> std::enable_if_t<std::is_convertible<T, char const*>{}> sink(T s); sink(msg); // #2が呼び出される sink("Hello"); // #1が呼び出される
ノート:GCC 7.2, Clang 5.0.0, MSVC 14.0で試した限りでは、後述(enable_if
メタ関数を使わない)デフォルトテンプレートパラメータ指定つき関数テンプレートでも期待通りに動作する。ただしT
が意図しない型へ推論されるリスクは残るため、やはりenable_if
メタ関数はあったほうが無難か。
// #2 デフォルトでT=const char*をとる関数テンプレート template <typename T = const char*> void sink(T s);
関連URL