yohhoyの日記

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

Concept-basedオーバーロードとSFINAE-unfriendlyメタ関数の落とし穴

C++2a(C++20)で導入されるrequires節を用いた関連制約(associated constraints)により従来SFINAE技法よりも関数テンプレートのオーバーロード制御が容易となるが、戻り値型にSFINAE-unfriendlyなメタ関数を用いるケースでは意図しないコンパイルエラーを引き起こす。

2021-01-13追記:CWG 2369. Ordering between constraints and substitution にてConceptとSFINAEの差異をなくす修正提案がなされ、N4879 によれば DefectReport として採択済み。C++20から遡及適用されるため、本記事の内容は全て無効となる。☆(ゝω・)vヤッタネ

本記事の内容はStackOverflowで見つけた質問と回答に基づく。

下記コードにおいてdouble型の値1.0test関数を呼び出すと、オーバーロード#1の戻り値型導出にて make_signedメタ関数 の必須要件(Mandates)違反によるコンパイルエラーとなる(→id:yohhoy:20200605)。*1

// C++2a
#include <concepts>
#include <type_traits>

// template <typename T>
//   requires std::unsigned_integral<T>と等価
template <std::unsigned_integral T>
auto test(T) -> std::make_signed_t<T>;  // #1

template <typename T>
auto test(T) -> int;  // #2

test(1u);   // OK: #1を選択
test(1.0);  // NG!? (#2を期待)

この振る舞いは 1) 実引数からのテンプレートパラメータ推論とオーバーロード候補除外(SFINAE-based overloading)、2) テンプレートパラメータに依存する戻り値型導出、3) 関連制約に基づくオーバーロード候補除外(Concept-based overloading)という順序に起因している。

前掲コードを古き良きSFINAE技法に書き換えると、オーバーロード#1の戻り値型導出よりも前に候補除外されstd::make_signedメタ関数の必須要件違反は回避される。一方でrequires節に比べると煩雑なコード記述が必要とされる。

#include <concepts>
#include <type_traits>

template <typename T,
          typename = std::enable_if_t<std::unsigned_integral<T>>>
auto test(T) -> std::make_signed_t<T>;  // #1

template <typename T,
          typename = std::enable_if_t<!unsigned_integral<T>>>
auto test(T) -> int;  // #2

test(1u);   // OK: #1を選択
test(1.0);  // OK: #2を選択

StackOverflow回答によれば本件は CWG 2369 にて議論中とのこと。2020年9月現在は最新ステータス非公開...

C++2a DIS(N4861) 13.10.2/p5より引用(下線部は強調)。

The resulting substituted and adjusted function type is used as the type of the function template for template argument deduction. If a template argument has not been deduced and its corresponding template parameter has a default argument, the template argument is determined by substituting the template arguments determined for preceding template parameters into the default argument. If the substitution results in an invalid type, as described above, type deduction fails. [Example:

 template <class T, class U = double>
 void f(T t = 0, U u = 0);

 void g() {
   f(1, 'c');      // f<int,char>(1,'c')
   f(1);           // f<int,double>(1,0)
   f();            // error: T cannot be deduced 
   f<int>();       // f<int,double>(0,0)
   f<int,char>();  // f<int,char>(0,0)
}

-- end example]
When all template arguments have been deduced or obtained from default template arguments, all uses of template parameters in the template parameter list of the template and the function type are replaced with the corresponding deduced or default argument values. If the substitution results in an invalid type, as described above, type deduction fails. If the function template has associated constraints (13.5.2), those constraints are checked for satisfaction (13.5.1). If the constraints are not satisfied, type deduction fails.

関連URL

*1:実際の振る舞いはC++標準ライブラリ内部実装に依存する。処理系によってはオーバーロード#1が ill-formed とならずに、期待通りオーバーロード#2が選択される可能性もある。