std::void_t
void_t
とは以下のようなエイリアステンプレートです。 な感じのメタ関数になります。
template<typename Ts...> using void_t = void;
あらゆる型のベクトルを受け取りvoidへ写します。こんなもんが一体何の役に立つというのか?というと、detection idiomと呼ばれる手法に使われます(というかそれ以外の使い方を知らない)。
detection idiom
detection idiomとはvoid_t
を利用して、ある型が持つ特性や適用可能な操作をコンパイル時に検出する手法です。ググると以下のような感じの典型的な実装が出てくるかと思います。
template<class, class = std::void_t> struct is_equality_comparable : std::false_type {}; template<class T> struct is_equality_comparable<T, std::void_t<decltype(std::declval<const T&>() == std::declval<const T&>())> : std::true_type {};
この例だと、同じ型を引数にとるoperator==を実装しているか(等値比較可能か)を検出しています。
このままだと、operator==のチェック部分とvoid_t
による検出部分がくっついていて見づらいため、それらを分離してみます。
template<class, template<class> class, class = std::void_t<>> struct detect : std::false_type {}; template<class T, template<class> class Check> struct detect<T, Check, std::void_t<Check<T>>> : std::true_type {};
template<class T> using is_equality_comparable_checker = decltype(std::declval<const T&>() == std::declval<const T&>()); template<class T> using is_equality_comparable = detect<T, is_equality_comparable_checker>;
detect
クラスがvoid_t
を利用して特性の検出を行う部分、is_equality_comparable_checker
エイリアステンプレートが型Tを受け取り、operator==の存在チェックを行う部分になります。
このdetectクラスにある型TとTを一つ引数にとるメタ関数(この場合はis_equality_comparable_checker
)を渡すことで、そのメタ関数が実行可能かによって任意の性質をチェックします。
例えばある型について足し算が可能かをチェックするには次のようなメタ関数を作ってやります。
template<class T> using is_addable_checker = decltype(std::declval<const T&>() + std::declval<const T&>()); template<class T> using is_addable = detect<T, is_addable_checker>;
たったのこれだけ、検出のキモはdetect
クラスとvoid_t
にあり、detect
クラスではテンプレートの部分特殊化とSFINAEを利用してfalse_typeとtrue_typeの分岐を行っています。
void_t
を主に利用しているのはstd::true_type
を継承する方のdetect
(部分特殊化)で、チェックする一変数メタ関数(Check<T>
)を実行しその戻り値型をvoidへ写しています。
template<class T, template<class> class Check> struct detect<T, Check, std::void_t<Check<T>>> : std::true_type {};
ここで、もしもCheck<T>
が失敗する(=TがCheckで調べられている特性を持っていない)場合はvoid_t
はエラーとなりSFINAEによってプライマリテンプレート(std::false_type
を継承する方)が選択されます。
Check<T>
が恙なく評価されれば、その戻り値型が何であれvoidへ写して終了。結果、プライマリテンプレートと部分特殊化のシグネチャが同じになり、部分特殊化が優先的に選択され、std::true_type
を継承したdetect
が実体化されます。
void_t
はその型引数が全てエラー無く評価出来たらvoidに、エラーが出たなら自身もエラーとなりSFINAEを起動する、そのための核となっているわけです。
int_tあるいは任意のT_t
ここで疑問に持たれた方もいるかもしれません。なぜvoid_t
なのか?int_t
やその他の型ではダメなのか?と。
結論から言えば、int_t
でもなんでも良いのです。必要なのは、あらゆる型をある一つの型に写すという性質と、与えられた型引数がエラーとなるときは自身もエラーとなる(SFINAEのトリガーとなる)、この2つの性質なのです。
int_t
を試してみましょう。detect
クラスを以下のように修正します。
template<class...> using int_t = int; template<class, template<class> class, class = int_t<>> struct detect : std::false_type {}; template<class T, template<class> class Check> struct detect<T, Check, int_t<Check<T>>> : std::true_type {};
void_t
と同じ結果を得られています。void_t
と同じようにint_t
を実装し、void_t
を置き換えてやっただけです。
T_t
にしてもおkです。
この様に別に何_tでもいいのですが、void
が選ばれたのはvoid
という型がこのような性質を表現するのに最適であるからだと思われます(引数は捨てるし、写した結果も使用しない)。
クラステンプレートの部分特殊化とオーバーロード解決
ところで、例えばint_t
ではプライマリテンプレートの最後のデフォルトパラメータ以下のようにしても動きます。
template<class, template<class> class, class = int> struct detect : std::false_type {};
これはvoid_t
の場合も同じ(= void
にする)です。しかし、int_t
ならint
、void_t
ならvoid
、T_t
ならT
にしないといけません(もしくは、最終的にならないといけない)。それ以外の型を指定しておくと、必ずプライマリテンプレートが選択されるようになります。なぜでしょうね・・・
これを理解するにはプライマリテンプレートと部分特殊化があるとき、どの様にそのオーバーロードが解決されるのかを知らねばなりません。
クラステンプレートが実体化が必要な個所で使用された場合、プライマリテンプレートと部分特殊化のうちの一つから最適な物を選択します。その時、与えられた実際の型の実引数列が部分特殊化の特殊化された仮引数列にどれだけ一致しているかを見ることで選択されます。
- 一致する部分特殊化が一つだけ見つかった場合は、それを選択。
- 一致する部分特殊化が複数見つかった場合は、半順序規則により最も特殊化されている部分特殊化を選択する。最も特殊化された部分特殊化が複数ある場合はコンパイルエラー。
- 一致する部分特殊化が見つからなかった場合、プライマリテンプレートが選択される。
半順序規則はここでは関係ないので説明を省きます(というかできません・・・)。
この選択ルールからまずわかることは、与えられた型引数にプライマリテンプレートと部分特殊化が両方マッチするとき、部分特殊化が優先されるということです(プライマリテンプレートの優先度は最低)。これにより、detection idiomにおいてstd::true_type
を継承する方が常に部分特殊化になっていることと、それが選択される理由が分かります。
次にプライマリテンプレートの最後のデフォルト引数をT_t
のT
と一致しておく必要があること、ですが、これはコンパイラのお気持ちになって実引数と仮引数のマッチングを考えれば分かります。void_t
を使ったdetect
クラスで考えてみます。
例えばis_addable<int>
を呼ぶと、detect
は以下のように呼ばれます。
detect<int, is_addable_checker>;
この時、引数は2つしか渡していないから部分特殊化を選択、とはなりません。呼び出しが適格であるためには、少なくともプライマリテンプレートのシグネチャに合っていなければなりません。プライマリテンプレートは3引数で宣言されているので3つ目の引数をまず充填します。どこからというと、プライマリテンプレートのデフォルト引数(すなわちvoid
)を入れます(デフォルト引数が無いとコンパイルエラー)。
結果、呼び出されたdetect
のシグネチャはdetect<int, is_addable_checker, void>
となります。
次にこのシグネチャで部分特殊化を見に行きましょう。とはいえ一つしかないのでそれの仮引数列と上の実引数列をマッチングします。
template<class T, template<class> class Check> struct detect<T, Check, std::void_t<Check<T>>> : std::true_type {};
2つ目の引数までは問題ありませんが3つ目の引数はメタ関数の結果をvoid_t
で写した結果(つまりvoid
)が利用されます。なのでそこを展開しましょう。is_addable_checker<int>
はint
の足し算の結果の型(int
)で定義されます。これは何のエラーも起こりませんので結果int
が帰り、void_t
によってvoid
にされます。
結果、部分特殊化のシグネチャはdetect<int, is_addable_checker, void>
となり、呼び出されたシグネチャと見事に一致するので部分特殊化が選択されます。
なお、もしここでis_addable_checker<T>
が失敗する(Tが足し算できない)場合、部分特殊化全体はエラーとなり、SFINAEによって候補から除外されます。結果、探すべき部分特殊化は無くなるので、選択ルールの3番目によりプライマリテンプレートが選択されます。
さて、ここでdetect
の3番目の引数をint
とでも置いてみましょう。
template<class, template<class> class, class = int> struct detect : std::false_type {};
この状態で先ほどと同じように呼び出し、まずプライマリテンプレートから3つ目の引数を取得します。
結果、呼び出されたシグネチャはdetect<int, is_addable_checker, int>
となります。
次に部分特殊化のシグネチャを求めます、ここは先ほどとは変わらないため、部分特殊化のシグネチャはdetect<int, is_addable_checker, void>
となります。
この2つを比較してみますと3番目の引数が一致しません。よって部分特殊化はマッチングしているとはみなされなくなり、残りの部分特殊化も無いためプライマリテンプレートが選択されます。
このため、プライマリテンプレートのデフォルト引数はT_t
のT
と一致している必要があり、無くてはならないものなのです。
void_t
の誕生
(一旦void_t
を忘れて)このような規則の下で、部分特殊化を利用してあるメタ関数が成功する場合と失敗する場合とで選択されるクラステンプレートを切り替えることを考えてみます。
プライマリテンプレートに対して部分特殊化を優先的に選択させるには、メタ関数成功時にプライマリテンプレートのシグネチャと一致している必要があります(失敗時はSFINAEに頼れば良い)。
メタ関数実行のためにテンプレートパラメータを余分に一つ受ける必要があるでしょう。SFINAEで選択される方(プライマリテンプレート)は実行の必要がないので、そこにデフォルトパラメータを設定しておけば良いでしょう。
しかし部分特殊化の方はその部分でメタ関数を実行するので、その結果をプライマリテンプレートのデフォルトパラメータに一致させる必要があります。
そのためには渡されるメタ関数の結果が何であれ最終的には予め予見できる何かしらの一つの型になってほしい。そのような何かが欲しい・・・
すなわちそれこそがvoid_t
です。
この様に見れば、void_t
とはクラステンプレートの部分特殊化オーバーロード解決をうまく利用するために必要不可欠なメタ関数であるという事と、detection idiomが上手く働く仕組みが分かるのではないでしょうか・・・?
参考文献
- std::void_t - cpprefjp
- Detection Idiom - yohhoyの日記
- Proposing Standard Library Support for the C++ Detection Idiom, v2
- 17.5.5 Class template partial specializations [temp.class.spec] - C++17 DIS N4659
- 14.4.5 クラステンプレートの部分的特殊化(Class template partial specializations) - C++11の文法と機能(C++11: Syntax and Feature)