site
site copied to clipboard
テンプレート引数として使用するtype_identity_tはなんのため?
いまこれの解説にとりかかっているのですが、テンプレート引数として使用されているtype_identity_t
がなんのために使われているのかわからず困っています。省略してもサンプルコード内では結果が変わらないように思います。
template <class T, class U>
struct C {
C(T, U); // #1
};
template <class T, class U>
C(T, U) -> C<T, std::type_identity_t<U>>; // #2 これ
type_identity
の提案文書にも、この利用方法はとくに書いていないように思います。
どなたかこの利用方法の理由がわかる方はいるでしょうか?
P1021R4の"Class Template Argument Deduction for alias templates"のセクションに、polymorphic_allocator
が導入される前にこの提案があったらpmr::vector
の宣言はこうなっていたのでは?的な事が書いてあって、そこで近い?使われ方をしています。
namespace pmr {
template<typename T>
using vector = std::vector<T, type_identity_t<pmr::polymorphic_allocator<T>>>; // See P0887R1 for type_identity_t
}
pmr::vector pv({1, 2, 3}, mem_res); // OK with this definition
テンプレートパラメータの推論対象外にする意図のようですが、よくわからない・・・
https://stackoverflow.com/questions/31942048/when-to-use-the-identity-tmp-trick にいろいろ書いてありますがうーん?
わからないのでstd-discussionに質問を投げてみました。 https://lists.isocpp.org/std-discussion/2020/12/0932.php
なるほど。
template <class T> void f(T a, T b);
だとf(4.2, 1);
でどちらの型に合わせればいいかわからなくて曖昧になるけど、template <class T> void f(T a, type_identity_t<T> b);
だと第1引数の型に推論される、と。
でも今回のケースは関係なさそうです。
polymorphic_allocator
の例は、mem_res
引数から直接推論されてしまうとvector<T, memory_resource*>
になってしまうけど、意図としてはmem_res
を与えてもvector<T, polymorphic_allocator<T>>
に推論されてほしいというもののようです。
コンストラクタの第2引数になにが渡されようとも、クラステンプレートの第2テンプレートパラメータの型はすでに確定しているからコンストラクタ引数から推論はされず、mem_res
はpolymorphic_allocator<T>
型の変換コンストラクタの引数として扱われる、と。
#include <iostream>
#include <memory_resource>
#include <type_traits>
template <class T, class Allocator=std::allocator<T>>
class my_vector {
public:
my_vector(std::initializer_list<T>, Allocator) {}
};
template <class T>
using my_pmr_vector = my_vector<T, std::type_identity_t<std::pmr::polymorphic_allocator<T>>>;
// コンストラクタが、
// my_vector(std::initializer_list<T>, Allocator) {}
// こうなって、
// my_vector(std::initializer_list<T>, std::type_identity_t<std::pmr::polymorphic_allocator<T>>) {}
// こうなるから、
// my_vector(std::initializer_list<T>, std::pmr::polymorphic_allocator<T>) {}
// mem_resはstd::pmr::polymorphic_allocator<T>のコンストラクタ引数として扱われる
int main() {
std::pmr::memory_resource* mem_res = nullptr;
my_pmr_vector vec{{1, 2, 3}, mem_res};
static_assert(std::is_same_v<decltype(vec), my_vector<int, std::pmr::polymorphic_allocator<int>>>);
}
規格のこの例は抽象的すぎて意味がわからないのと、とくに意味がないように思えるので、より具体的な例を紹介すればよさそうな気がします。
template <class T, class U> struct C {
C(T, U); // #1
};
template<class T, class U>
C(T, U) -> C<T, std::type_identity_t<U>>; // #2
込み入った利用方法なのでpmr::vector
以外のわかりやすい例を考えるのがつらいとこです。(だから規格はこんな例になってるのかもしれませんが)
template <class T>
using my_pmr_vector = my_vector<T, std::pmr::polymorphic_allocator<T>>;
type_identity_t
を通さない場合は、コンストラクタ引数{1, 2, 3}, mem_res
を渡したいずれのコンストラクタ・推論ガイドの結果もmy_vector<T, std::pmr::polymorphic_allocator<T>>;
型を導出しないので、曖昧になって推論失敗になるようです。
std::type_identity_t
を各種コンテナのアロケータパラメータに適用して、pmr
コンテナの用法を改善する提案が出てました。ご参考に。
ここでのstd::type_identity_t
の使用は、エイリアステンプレートの推論補助を導出する際に推論できないコンテキストを意図的に作り出すことが目的で、それがあるときでも推論補助の導出が失敗しない・エイリアステンプレートの意味が変わるような推論補助とならない、ことを例示したいのだと思われます。
規格書(提案文書)の最初に出てくる例、クラスC
で推論補助を求めてみると、その有無で導出される推論補助が異なることがわかります。
手順などの詳細は省略するので、この記事をご参照ください。
std::type_identity_t
がある時
template <class T, class U>
struct C {
C(T, U);
};
// (1) 推論補助
template<class T, class U>
C(T, U) -> C<T, std::type_identity_t<U>>;
// (2) エイリアステンプレート
template<class V>
using A = C<V*, V*>;
今回は(1)の推論補助のみを対象に、推論補助の変換を行います。
まず(1)と(2)の右辺の対応から、テンプレートパラメータの対応を求めます。そこでは、(1)のテンプレートパラメータおよび右辺の型を引数とした関数テンプレート(f
とします)に対して、(2)の右辺の型を引数として与えた時の関数テンプレートのテンプレートパラメータ推論とほぼ同じことが行われます。
template<class T, class U>
f(C<T, std::type_identity_t<U>>);
f(C<V*, V*>{});
この時、f
のテンプレートパラメータ推論によってT = V*
の対応が求められます。一方、V*
とstd::type_identity_t<U>
からU
を推論できない(推論できないコンテキスト)ので、U
に関する対応は得られません。
次に、得たテンプレートパラメータの対応(T = V*
)とエイリアステンプレート(2)のテンプレートパラメータを推論補助(1)へフィードバックします。
// (2)のテンプレートパラメータのフィードバック
template<class V, class T, class U>
C(V*, U) -> C<V*, std::type_identity_t<U>>;
// T = V*のフィードバック
template<class V, class U>
C(V*, U) -> C<V*, std::type_identity_t<U>>;
この推論補助は第一引数と第二引数が同じでなくても良く、それによってC
のT, U
も異なってしまい、(2)と意味が変わります。そこで、結果が一貫するように(正確には、推論できないコンテキストがあっため)追加の制約が必要になります。
template<class V, class U>
C(V*, U) -> C<V*, std::type_identity_t<U>>
requires std::same_as<A<V>, C<V*, std::type_identity_t<U>>;
エイリアステンプレートの実引数を推定してエイリアステンプレートを書き戻す過程は省略し、これがエイリアステンプレート(2)に対して元の型の推論補助(1)を変換して得られる推論補助になります。
std::type_identity_t
がない時
template <class T, class U>
struct C {
C(T, U);
};
// (1) 推論補助
template<class T, class U>
C(T, U) -> C<T, U>;
// (2) エイリアステンプレート
template<class V>
using A = C<V*, V*>;
まず(1)と(2)の右辺の対応から、テンプレートパラメータの対応を求めます。
template<class T, class U>
f(C<T, U>);
f(C<V*, V*>{});
ここでは、T, U
ともに推論可能なコンテキストなので、f
のテンプレートパラメータT = U = V*
と対応が求められます。
次に、得たテンプレートパラメータの対応(T = U = V*
)とエイリアステンプレート(2)のテンプレートパラメータを推論補助(1)へフィードバックします。
// (2)のテンプレートパラメータのフィードバック
template<class V, class T, class U>
C(T, U) -> C<T, U>;
// T = U = V*のフィードバック
template<class V>
C(V*, V*) -> C<V*, V*>;
今回は推論できないコンテキストはなかったため追加の制約は不要で、これがエイリアステンプレート(2)に対して元の型の推論補助(1)を変換して得られる推論補助になります。
差
2つのケースを比べてみると
// type_identity_tがある場合の推論補助
template<class V, class U>
C(V*, U) -> C<V*, std::type_identity_t<U>>
requires std::same_as<A<V>, C<V*, std::type_identity_t<U>>;
// type_identity_tがない場合の推論補助
template<class V>
C(V*, V*) -> C<V*, V*>;
このように、std::type_identity_t
が推論補助右辺に現れていることによって(正確には、推論できないコンテキストが発生することで)、導出される推論補助は変化します。ただ、この場合は得られる結果は変化しないはずです。
GCCのやること
GCCでは、推論補助を使用した時に推論に失敗してエラーとなると、エラーメッセージ中に使用した推論補助を出力してくれます。
template <class T, class U>
struct C {
C(T, U);
};
template<class T, class U>
C(T, U) -> C<T, U>;
template<class V>
using A = C<V*, V*>;
int main() {
int i{};
double d{};
A a{&i, &d}; // error
}
-
std::type_identity_t
がある場合 - [Wandbox]三へ( へ՞ਊ ՞)へ ハッハッ -
std::type_identity_t
がない場合 - [Wandbox]三へ( へ՞ਊ ՞)へ ハッハッ
そこから拾って先ほどの場合にそれぞれどのような推論補助が使用されているかみてみると
// type_identity_tがある場合
template<class V, class U>
C(V*, U) -> C<V*, typename std::type_identity<U>::type>
requires __is_same(A<V>, C<V*, typename std::type_identity<U>::type>);
// type_identity_tがない場合
template<class V>
C(V*, V*)-> C<V*, V*>;
となっており、手で求めたものと一致しており、やはり導出される推論補助が異なっていることが確認できます。
これらのことから、規格書およびP1814のサンプルコードでstd::type_identity_t
が使用されているのは、推論補助導出過程で意図的に推論できないコンテキストを作り出すこと、それによって追加の制約が必要となること、その差で結果が変わらないこと、を例示しているのだと考察しました。