templateの一致度とオーバーロード
SFINAE を用いたライブラリの作成を手伝う機会があり、そのときtemplateの一致度について理解してもらうのに2時間ほどかかった。その後、一致度の違いによって関数が選択されるところに食いつかれて更に1時間かかったので、templateの一致度がより高まる順に表にしてまとめる。
これを使えばオーバーロードするときにどれが選ばれるか把握したり、同じ一致度のものを作って多重定義になる問題を避けられる。
わかってる人が早見に使いたいとき用ということでtemplateの細かい説明は省略する。
説明環境
解説用のコードはc++11を理解できる人を想定している。早見表自体はc++03からc++14まで利用できる(はず)。
一致度早見表(クラス)
- 仮引数の一致
- 特殊化の一致
- 部分特殊化の一致
一度早見表(関数)
- 仮引数の一致
- テンプレートの解釈の手間が少ないやつ(?)
- テンプレートが関係しない中でキャストして選べるもの
早見表の解釈
テンプレートはコンパイル時にすべて解釈されるので、解釈後の型の状態を思い浮かべる。
そこから1.で一つに決まらなければ2.で決めていくという動作をする。
例えば、次のように仮引数が同じ関数が三つあるとする。
template<typename T, typename S>
bool func(const T& lhs, const S& rhs); // (1)
template<typename T>
bool func(const T& lhs, const T& rhs); // (2)
template<>
bool func(const int& lhs, const int& rhs); // (3)
それぞれきちんと定義すれば上はコンパイル可能であり正常に動作する。
よって、次のコードは期待通り動作する。
const int a1 {0}, a2 {1};
func(a1, a2); // call (3)
const double b1 {0.1}, b2 {3.2};
func(b1, b2); // call (2)
func(a1, b2); // call (1)
陥りやすい問題例
const lvalue と Universal reference の一致度
早見表にあるように仮引数が一致した場合はその時点で関数が絞り込まれる。
次の例ようにあらゆる型を受け取るが、実際の処理はある型に変換してから行う場合には直観に反しやすい。
特にconst lvalueはどのような参照でも受け取ることができるが、一致度の解釈では珍しい型const lvalueに限定して解釈される。
#include <string>
template<typename charT>
func(const std::basic_string<typename charT>&); // (4)
template<typename T>
func(T&& value)
{
func(std::string(std::forward<T>(value))) // expect calling (4) ... Miss [1]
}
std::string incorrect("except call (4)");
func(incorrect); // Miss [2]
const std::string correct("call (4) correctly");
func(correct); // call (4)
残念ながらMissの結果はユニバーサル参照の無限呼び出しで終わってしまう。
Miss [1]
(4)が const std::string&
(const の string の左辺値参照)の仮引数と解釈するのに対して、
ユニバーサル参照の方は std::string&&
(string の右辺値参照)の仮引数を持ってしまう。
Miss [1] の呼び出しはその場で std::string
を呼び出しているので右辺値であるし、constでもない。
このため、仮引数の一致するユニバーサル参照の関数が再帰されてしまう。
Miss [2]
(4)が const std::string&
(const の string の左辺値参照)の仮引数と解釈するのに対して、
ユニバーサル参照の方は std::string&
(string の左辺値参照)の仮引数を持ってしまう。
Miss [2] の呼び出しはstd::stringの左辺値で行っているが、(4)の仮引数にはconstがついている。
このため、仮引数の一致するユニバーサル参照が選ばれてしまう。
templateクラスと部分特殊化見間違い
SFINAEではtemplateクラスと部分特殊化を利用して書いてゆくが、templateクラスと部分特殊化の構文が似ていて勘違いされてしまった。
#include <type_traits>
template <typename T, typename = void>
class has_value_type
: public std::false_type
{};
template <typename T>
class has_value_type<T, typename std::conditional<false, typename T::value_type, void>::type>
: public std::true_type
{};
この例で、上はtemplateクラス has_value_type
の定義であり、下はその部分特殊化 has_value_type<T, void>
の定義である。
テンプレート引数Tを一つだけ与えて has_value_type<T>
を具現化すると、
上のtemplateクラスの定義によって has_value_type<T, void>
と解釈される。
この型は下の部分特殊化によって具体化を試みられ、T::value_type
が存在すれば具体化に成功し、true_typeを継承する。
T::value_type
が存在しなければSFINAEによって下が無視され、残ったtemplateクラスの定義によってfalse_typeを継承する。
部分特殊化が見づらいのは、教えていた相手曰くtemplateの片方がデフォルト値を利用していて解釈後がどちらも同じに見えるかららしい。特殊化によるオーバーロード分岐はよく使うテクニックであるが、SFINAEに慣れていないと全ての場合を上書きしているように見えるようだ。実際には同じでなければ困るが。
それはさておき見極め方は名前の後ろに特殊化の指定があるかどうかである。templateと連続で並んでいると目が滑りがちなので注意して読むとよい。
この内容を通話越しに教えるのに1時間かかってしまったので、次回からは一致度の高い関数が選ばれるという部分の理解を確かめてから教えるよう心がけたい。