Skip to content

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

Open

Description

@faithandbrave

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

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

Activity

  1. onihusube commented on Dec 17, 2020

    @onihusube
    Member

    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

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

  2. yumetodo commented on Dec 17, 2020

    @yumetodo
    Member
  3. faithandbrave commented on Dec 18, 2020

    @faithandbrave
    MemberAuthor

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

  4. faithandbrave commented on Dec 21, 2020

    @faithandbrave
    MemberAuthor

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

  5. faithandbrave commented on Dec 21, 2020

    @faithandbrave
    MemberAuthor

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

  6. faithandbrave commented on Dec 21, 2020

    @faithandbrave
    MemberAuthor

    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以外のわかりやすい例を考えるのがつらいとこです。(だから規格はこんな例になってるのかもしれませんが)

  7. faithandbrave commented on Dec 21, 2020

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

  8. onihusube commented on Mar 27, 2021

    @onihusube
    Member

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

  9. onihusube commented on Nov 11, 2021

    @onihusube
    Member

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

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

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions