[C++]void_tとその周辺

std::void_t

void_tとは以下のようなエイリアステンプレートです。f:Ts... \to void な感じのメタ関数になります。

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>;

[Wandbox]三へ( へ՞ਊ ՞)へ ハッハッ

たったのこれだけ、検出のキモは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 {};

[Wandbox]三へ( へ՞ਊ ՞)へ ハッハッ

void_tと同じ結果を得られています。void_tと同じようにint_tを実装し、void_tを置き換えてやっただけです。

T_tにしてもおkです。

[Wandbox]三へ( へ՞ਊ ՞)へ ハッハッ

この様に別に何_tでもいいのですが、voidが選ばれたのはvoidという型がこのような性質を表現するのに最適であるからだと思われます(引数は捨てるし、写した結果も使用しない)。

クラステンプレートの部分特殊化とオーバーロード解決

ところで、例えばint_tではプライマリテンプレートの最後のデフォルトパラメータ以下のようにしても動きます。

template<class, template<class> class, class = int>
struct detect : std::false_type {};

これはvoid_tの場合も同じ(= voidにする)です。しかし、int_tならintvoid_tならvoidT_tならTにしないといけません(もしくは、最終的にならないといけない)。それ以外の型を指定しておくと、必ずプライマリテンプレートが選択されるようになります。なぜでしょうね・・・

これを理解するにはプライマリテンプレートと部分特殊化があるとき、どの様にそのオーバーロードが解決されるのかを知らねばなりません。

クラステンプレートが実体化が必要な個所で使用された場合、プライマリテンプレートと部分特殊化のうちの一つから最適な物を選択します。その時、与えられた実際の型の実引数列が部分特殊化の特殊化された仮引数列にどれだけ一致しているかを見ることで選択されます。

  1. 一致する部分特殊化が一つだけ見つかった場合は、それを選択。
  2. 一致する部分特殊化が複数見つかった場合は、半順序規則により最も特殊化されている部分特殊化を選択する。最も特殊化された部分特殊化が複数ある場合はコンパイルエラー。
  3. 一致する部分特殊化が見つからなかった場合、プライマリテンプレートが選択される。

半順序規則はここでは関係ないので説明を省きます(というかできません・・・)。

この選択ルールからまずわかることは、与えられた型引数にプライマリテンプレートと部分特殊化が両方マッチするとき、部分特殊化が優先されるということです(プライマリテンプレートの優先度は最低)。これにより、detection idiomにおいてstd::true_typeを継承する方が常に部分特殊化になっていることと、それが選択される理由が分かります。

次にプライマリテンプレートの最後のデフォルト引数をT_tTと一致しておく必要があること、ですが、これはコンパイラのお気持ちになって実引数と仮引数のマッチングを考えれば分かります。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_tTと一致している必要があり、無くてはならないものなのです。

void_tの誕生

(一旦void_tを忘れて)このような規則の下で、部分特殊化を利用してあるメタ関数が成功する場合と失敗する場合とで選択されるクラステンプレートを切り替えることを考えてみます。

プライマリテンプレートに対して部分特殊化を優先的に選択させるには、メタ関数成功時にプライマリテンプレートのシグネチャと一致している必要があります(失敗時はSFINAEに頼れば良い)。

メタ関数実行のためにテンプレートパラメータを余分に一つ受ける必要があるでしょう。SFINAEで選択される方(プライマリテンプレート)は実行の必要がないので、そこにデフォルトパラメータを設定しておけば良いでしょう。

しかし部分特殊化の方はその部分でメタ関数を実行するので、その結果をプライマリテンプレートのデフォルトパラメータに一致させる必要があります。

そのためには渡されるメタ関数の結果が何であれ最終的には予め予見できる何かしらの一つの型になってほしい。そのような何かが欲しい・・・

すなわちそれこそがvoid_tです。

この様に見れば、void_tとはクラステンプレートの部分特殊化オーバーロード解決をうまく利用するために必要不可欠なメタ関数であるという事と、detection idiomが上手く働く仕組みが分かるのではないでしょうか・・・?

参考文献

この記事のMarkdownソース