yohhoyの日記

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

オーバーロード解決の優先順位制御

プログラミング言語C++における関数テンプレートのオーバーロードにおいて、SFINAEと組み合わせてオーバーロード解決の優先順を制御するテクニック。

選択候補が2個のケース

2つの型T, Uに対して、1) 演算 T / U が定義されていれば同演算子を、2) そうでなければ T * (1/U) を選択するケースを考える。*1

// 実装関数 第1候補
template <typename T, typename U>
auto f_impl(T a, U b, int) -> decltype(a / b)
{ return a / b; }

// 実装関数 第2候補
template <typename T, typename U>
auto f_impl(T a, U b, char) -> decltype(a * (U{1} / b))
{ return a * (U{1} / b); }

// 公開関数
template <typename T, typename U>
auto f(T a, U b)
{ return f_impl(a, b, 0); }

実装関数テンプレートの第3引数に 1)int型、2)char型 を指定し、f_impl呼び出し側で 0 指定により優先順制御を行う。関数のオーバーロード解決(overload resolution)において、リテラル 0int型として扱われる規則が、char型として扱われる規則より優先されるため。*2

選択候補がN個のケース

優先順位付けを表現するrank<I>ヘルパクラスを導入する。公開関数で指定するrank<2>実引数に対して、オーバーロード解決時には基底クラスrank<1>よりもrank<2>へと優先的にマッチするため。同様にrank<0>よりもrank<1>が優先する。

template <int I> struct rank : rank<I-1> {};
template <> struct rank<0> {};

// 実装関数 第1候補
template <typename T, typename U>
auto f_impl(T a, U b, rank<2>) -> decltype(a / b)
{ return a / b; }

// 実装関数 第2候補
template <typename T, typename U>
auto f_impl(T a, U b, rank<1>) -> decltype(a * (U(1) / b))
{ return a * (U(1) / b); }

// 実装関数 第3候補
template <typename T, typename U>
int f_impl(T a, U b, rank<0>)
{ return 0; }

// 公開関数
template <typename T, typename U>
auto f(T a, U b)
{ return f_impl(a, b, rank<2>{}); }

メモ:Web上で本テクニックを利用するコードをちらほら見かける。名前の付いたIdiomではないのだろうか?

関連URL

*1:ここでは公開関数 f でのみ通常関数での戻り値型推論を利用している。実装関数 f_impl の末尾戻り値型宣言を省略するとSFINAEが機能せずに、式 T / U の有効有無にかかわらず1)にオーバーロード解決されてしまう。

*2:リテラル 0 はC++14 2.14.2/p2よりint型と解釈される。優先順位は13.3.3.1.1のstandard conversion sequenceにある1) No conversions required → Eact Match Rank と2) Integral conversions → Conversion Rank に基づく…ような気がする。オーバーロード解決規則は難解すぎて理解できない。