site icon indicating copy to clipboard operation
site copied to clipboard

テンプレート引数として使用するtype_identity_tはなんのため?

Open faithandbrave opened this issue 3 years ago • 9 comments

いまこれの解説にとりかかっているのですが、テンプレート引数として使用されている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の提案文書にも、この利用方法はとくに書いていないように思います。

どなたかこの利用方法の理由がわかる方はいるでしょうか?

faithandbrave avatar Dec 17 '20 08:12 faithandbrave

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

テンプレートパラメータの推論対象外にする意図のようですが、よくわからない・・・

onihusube avatar Dec 17 '20 08:12 onihusube

https://stackoverflow.com/questions/31942048/when-to-use-the-identity-tmp-trick にいろいろ書いてありますがうーん?

yumetodo avatar Dec 17 '20 11:12 yumetodo

わからないのでstd-discussionに質問を投げてみました。 https://lists.isocpp.org/std-discussion/2020/12/0932.php

faithandbrave avatar Dec 18 '20 06:12 faithandbrave

なるほど。 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引数の型に推論される、と。

faithandbrave avatar Dec 21 '20 16:12 faithandbrave

でも今回のケースは関係なさそうです。

faithandbrave avatar Dec 21 '20 17:12 faithandbrave

polymorphic_allocatorの例は、mem_res引数から直接推論されてしまうとvector<T, memory_resource*>になってしまうけど、意図としてはmem_resを与えてもvector<T, polymorphic_allocator<T>>に推論されてほしいというもののようです。

コンストラクタの第2引数になにが渡されようとも、クラステンプレートの第2テンプレートパラメータの型はすでに確定しているからコンストラクタ引数から推論はされず、mem_respolymorphic_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以外のわかりやすい例を考えるのがつらいとこです。(だから規格はこんな例になってるのかもしれませんが)

faithandbrave avatar Dec 21 '20 21:12 faithandbrave

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>>;型を導出しないので、曖昧になって推論失敗になるようです。

faithandbrave avatar Dec 21 '20 21:12 faithandbrave

std::type_identity_tを各種コンテナのアロケータパラメータに適用して、pmrコンテナの用法を改善する提案が出てました。ご参考に。

onihusube avatar Mar 27 '21 09:03 onihusube

ここでの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>>;

この推論補助は第一引数と第二引数が同じでなくても良く、それによってCT, 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
}

そこから拾って先ほどの場合にそれぞれどのような推論補助が使用されているかみてみると

// 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が使用されているのは、推論補助導出過程で意図的に推論できないコンテキストを作り出すこと、それによって追加の制約が必要となること、その差で結果が変わらないこと、を例示しているのだと考察しました。

onihusube avatar Nov 11 '21 02:11 onihusube