yohhoyの日記

技術的メモをしていきたい日記

文字列リテラルとヌル終端文字列とで関数オーバーロード

プログラミング言語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

*1:非型テンプレートパラメータとして受け取るため、文字列長 N をコンパイル時定数として扱える。プログラム実行時のコストはゼロ。

*2:コンパイラの最適化処理によって関数がインライン展開され、かつ処理系が strlen 関数を特別扱いしているならば、コンパイル時に文字列長が算出される可能性はある。C++1z(C++17)では P0426R1 により std::char_traits<char>::length 関数が constexpr 指定されるため、C言語ライブラリ由来の strlen 関数よりも最適化され易い。たぶん。