クラステンプレートの実引数推定(C++17以上)
クラステンプレートを実体化するためには、すべてのテンプレート実引数が判明していなければなりませんが、すべてのテンプレート実引数を指定する必要はありません。 以下の文脈では、足りないテンプレート実引数をコンパイラが初期化子の型から推定します。
- 変数および変数テンプレートの初期化子を指定する任意の宣言。
std::pair p(2, 4.5);// std::pair<int, double> p(2, 4.5); と推定されます。std::tuple t(4, 3, 2.5);// auto t = std::make_tuple(4, 3, 2.5); と同じです。std::less l;// std::less<void> l; と同じです。
template<class T>struct A { A(T,T);};auto y = new A{1,2};// 確保される型は A<int> です。
auto lck =std::lock_guard(mtx);// std::lock_guard<std::mutex> と推定されます。std::copy_n(vi1, 3, std::back_insert_iterator(vi2));// または std::back_inserter(vi2)std::for_each(vi.begin(), vi.end(), Foo([&](int i){...}));// Foo<T> を推定します。 ただし// T は一意なラムダ型です。
template<class T>struct X { X(T){}auto operator<=>(const X&)const=default;}; template<X x>struct Y {}; Y<0> y;// OK、 Y<X<int>(0)> と同じです。 | (C++20以上) |
目次 |
[編集]暗黙に生成される推定ガイド
関数スタイルのキャストまたは変数の宣言が実引数のないプライマリクラステンプレート C
の名前を型指定子として使用したときは、以下のように推定がおこなわれます。
C
が定義されている場合は、そのプライマリテンプレートで宣言されているそれぞれのコンストラクタ (またはコンストラクタテンプレート)Ci
について、以下のような架空の関数テンプレートFi
が構築されます。
Fi
のテンプレート仮引数はC
のテンプレート仮引数の後に (Ci
がコンストラクタテンプレートの場合)Ci
のテンプレート仮引数 (デフォルト引数も含まれます) が続いたものです。Fi
の関数仮引数はコンストラクタの仮引数です。Fi
の戻り値の型はC
の後に <> で囲まれたクラステンプレートのテンプレート仮引数が続いたものです。
C
が定義されていないまたは何のコンストラクタも宣言されていない場合は、仮のコンストラクタC(C)
から上記のように派生した追加の架空の関数テンプレートが追加されます。- いずれの場合でも、仮のコンストラクタ
C(C)
から上記のように派生した追加の架空の関数テンプレートが追加されます (コピー推定候補と言います)。
その後、オーバーロード集合を形成する目的のために、ガイドとマッチするコンストラクタシグネチャ (戻り値の型は除きます) を持つ仮のクラスの架空のオブジェクトの初期化に対して、テンプレートの実引数推定とオーバーロード解決が行われます。 初期化子はクラステンプレートの実引数推定が行われた文脈で提供されます。 ただし、初期化子リストが型 U
(cv 修飾されていても構いません) の式ひとつから構成されている場合は、リスト初期化のフェーズ1 (初期化子リストコンストラクタの考慮) は省略されます。 ただし U
は C
の特殊化または C
の特殊化から派生したクラスです。
これらの仮のコンストラクタは架空のクラス型のパブリックメンバです。 これらは、ガイドが explicit コンストラクタから形成されている場合は、 explicit です。 オーバーロード解決が失敗した場合は、プログラムは ill-formed です。 そうでなければ、選択された F
テンプレート特殊化の戻り値が推定されたクラステンプレート特殊化になります。
template<class T>struct UniquePtr { UniquePtr(T* t);}; UniquePtr dp{new auto(2.0)};// 宣言されているコンストラクタ:// C1: UniquePtr(T*)// 暗黙に生成される推定ガイドの集合:// F1: template<class T> UniquePtr<T> F(T *p);// F2: template<class T> UniquePtr<T> F(UniquePtr<T>); // コピー推定候補// 初期化する架空のクラス:// struct X {// template<class T> X(T *p); // F1 から// template<class T> X(UniquePtr<T>); // F2 から// };// 「new double(2.0)」を初期化子として用いた、オブジェクト X の直接初期化は、// T = double と置いた、ガイド F1 に対応するコンストラクタを選択します。// T = double と置いた F1 は、戻り値の型が UniquePtr<double> です。// 結果:// UniquePtr<double> dp{new auto(2.0)}
あるいは、もっと複雑な例の場合 (ノート: 「S::N」はコンパイルできないことに注意してください。 スコープ解決修飾子は推定の対象ではありません):
template<class T>struct S {template<class U>struct N { N(T); N(T, U);template<class V> N(V, U);};}; S<int>::N x{2.0, 1};// 暗黙に生成される推定ガイド (T はすでに int と判明していることに注意):// F1: template<class U> S<int>::N<U> F(int);// F2: template<class U> S<int>::N<U> F(int, U);// F3: template<class U, class V> S<int>::N<U> F(V, U);// F4: template<class U> S<int>::N<U> F(S<int>::N<U>); (コピー推定候補)// 「{2.0, 1}」を初期化子として用いた直接初期化の場合のオーバーロード解決により、// U=int、 V=double と置いた F3 が選択されます。// その戻り値の型は S<int>::N<int> です。// 結果:// S<int>::N<int> x{2.0, 1};
[編集]ユーザ定義推定ガイド
ユーザ定義推定ガイドの構文は、後置戻り値型を用いた関数宣言の構文です。 ただし、関数名の代わりにクラステンプレートの名前を使用します。
explicit-specifier(オプション)template-name( parameter-declaration-clause) -> simple-template-id; | |||||||||
ユーザ定義推定ガイドは、クラステンプレートを指定しなければならず、そのクラステンプレートと同じ意味的スコープ (名前空間または囲っているクラス) で導入されなければならず、メンバクラステンプレートの場合は同じアクセスを持たなければなりませんが、推定ガイドはそのスコープのメンバにはなりません。
推定ガイドは関数ではなく、本体を持ちません。 推定ガイドは名前探索によって発見されず、クラステンプレートの実引数推定を行うときの他の推定ガイドに対するオーバーロード解決を除いて、オーバーロード解決に参加しません。 同じ翻訳単位内で同じクラステンプレートに対して推定ガイドを再宣言することはできません。
// テンプレートの宣言template<class T>struct container { container(T t){}template<class Iter> container(Iter beg, Iter end);};// 追加の推定ガイドtemplate<class Iter> container(Iter b, Iter e)-> container<typenamestd::iterator_traits<Iter>::value_type>;// 使用 container c(7);// OK、暗黙に生成されたガイドを使用して T=int を推定します。std::vector<double> v ={/* ... */};auto d = container(v.begin(), v.end());// OK、 T=double を推定します。 container e{5, 6};// エラー、 std::iterator_traits<int>::value_type は存在しません。
オーバーロード解決用の架空のコンストラクタ (前述の) は、 explicit コンストラクタから形成された暗黙に生成された推定ガイドに対応する場合、または explicit 宣言されたユーザ定義推定ガイドに対応する場合は、 explicit です。 いつも通り、そのようなコンストラクタはコピー初期化の文脈では無視されます。
template<class T>struct A {explicit A(const T&, ...)noexcept;// #1 A(T&&, ...);// #2}; int i; A a1 ={ i, i };// エラー、 #2 は右辺値参照からは推定できず、// #1 は explicit のためコピー初期化では考慮されません。 A a2{i, i};// OK、 #1 によって A<int> に推定され初期化されます。 A a3{0, i};// OK、 #2 によって A<int> に推定され初期化されます。 A a4 ={0, i};// OK、 #2 によって A<int> に推定され初期化されます。 template<class T> A(const T&, const T&)-> A<T&>;// #3template<class T>explicit A(T&&, T&&)-> A<T>;// #4 A a5 ={0, 1};// エラー、 #3 によって A<int&> に推定されますが、// #1 と #2 のコンストラクタが同じ引数になってしまいます。 A a6{0,1};// OK,、 #4 によって A<int> に推定され、 #2 によって初期化されます。 A a7 ={0, i};// エラー、 #3 によって A<int&> に推定されます。 A a8{0,i};// エラー、 #3 によって A<int&> に推定されます。
コンストラクタまたはコンストラクタテンプレートの引数リストでメンバ typedef またはエイリアステンプレートを使用することは、それ自体では、暗黙に生成されるガイドの対応する引数を非推定文脈にはしません。
template<class T>struct B {template<class U>using TA = T;template<class U> B(U, TA<U>);//#1}; // #1 による暗黙の推定ガイドは、以下と同等なものになります。// template<class T, class U> B(U, T) -> B<T>;// 以下ではありません。// template<class T, class U> B(U, typename B<T>::template TA<U>) -> B<T>;// 後者は推定できません。 B b{(int*)0, (char*)0};// OK、 B<char*> に推定されます。
[編集]ノート
クラステンプレートの実引数推定は、テンプレートの実引数リストが存在しない場合にのみ行われます。 テンプレートの実引数リストが指定されている場合は、推定は行われません。
std::tuple t1(1, 2, 3);// OK、推定されます。std::tuple<int,int,int> t2(1, 2, 3);// OK、すべての引数が指定されています。std::tuple<> t3(1, 2, 3);// エラー、 tuple<> にマッチするコンストラクタはありません。// 推定は行われません。std::tuple<int> t4(1, 2, 3);// エラー。
集成体のクラステンプレートの実引数推定は、一般的には、推定ガイドが必要です。
template<class A, class B>struct Agg {A a; B b;};// デフォルト、コピー、およびムーブコンストラクタから暗黙の推定ガイドが形成されます。template<class A, class B> Agg(A a, B b)-> Agg<A, B>; Agg agg{1, 2.0};// ユーザ定義のガイドから Agg<int, double> に推定されます。 template<class... T> array(T&&... t)-> array<std::common_type_t<T...>, sizeof...(T)>;auto a = array{1, 2, 5u};// ユーザ定義のガイドから array<unsigned, 3> に推定されます。
ユーザ定義推定ガイドはテンプレートである必要はありません。
template<class T>struct S { S(T);}; S(charconst*)-> S<std::string>; S s{"hello"};// S<std::string> に推定されます。
クラステンプレートのスコープ内では、引数リストを持たないテンプレート名は注入されたクラス名であり、型として使用できます。 この場合、クラステンプレートの実引数推定は発生せず、テンプレート引数は明示的に指定しなければなりません。
template<class T>struct X { X(T){}template<class Iter> X(Iter b, Iter e){} template<class Iter>auto foo(Iter b, Iter e){return X(b, e);// 推定は行われません。 X はここでは X<T> です。}template<class Iter>auto bar(Iter b, Iter e){return X<typename Iter::value_type>(b, e);// 欲しいものを指定しなければなりません。}auto baz(){return::X(0);// 注入されたクラス名でない。 X<int> に推定されます。}};
オーバーロード解決では、関数テンプレートがガイドから生成されたかどうかよりも、半順序が優先されます。 コンストラクタから生成された関数テンプレートが推定ガイドから生成されたものより特殊化されている場合は、コンストラクタから生成されたものが選択されます。 一般的には、コンストラクタのラッパーよりもコピー推定候補の方がより特殊化されているため、このルールは、一般的には、ラッパーよりもコピーを優先することを意味します。
template<class T>struct A { A(T, int*);// #1 A(A<T>&, int*);// #2enum{ value };};template<class T, int N = T::value> A(T&&, int*)-> A<T>;//#3 A a{1,0};// #1 を使用して A<int> を推定し、 #1 で初期化します。 A b{a,0};// #2 (#3 より特殊化されている) を使用して A<int> を推定し、 #2 で初期化します。
それまでのタイブレーカー (半順序を含む) が2つの候補関数テンプレートを区別できなかった場合は、以下のルールが適用されます。
- ガイドから生成された関数テンプレートは、コンストラクタまたはコンストラクタテンプレートから暗黙に生成されたものより、優先されます。
- コピー推定候補は、コンストラクタまたはコンストラクタテンプレートから暗黙に生成された他のすべての関数テンプレートより、優先されます。
- 非テンプレートコンストラクタから暗黙に生成された関数テンプレートは、コンストラクタテンプレートから暗黙に生成された関数テンプレートより、優先されます。
template<class T>struct A {using value_type = T; A(value_type);// #1 A(const A&);// #2 A(T, T, int);// #3template<class U> A(int, T, U);// #4};// A(A); #5 (コピー推定候補) A x (1, 2, 3);// #3 (非テンプレートコンストラクタから生成された) を使用します。 template<class T> A(T)-> A<T>;// #6 (#5 ほど特殊化されていない) A a (42);// #6 を使用して A<int> を推定し、 #1 を使用して初期化します。 A b = a;// #5 を使用して A<int> を推定し、 #2 を使用して初期化します。 template<class T> A(A<T>)-> A<A<T>>;// #7 (#5 と同程度に特殊化されている) A b2 = a;// #7 を使用して A<A<int>> を推定し、 #1 を使用して初期化します。
cv 修飾されていないテンプレート仮引数への右辺値参照は、その仮引数がクラステンプレートの仮引数である場合は、転送参照ではありません。
template<class T>struct A {template<class U> A(T&&, U&&, int*);// #1: T&& は転送参照ではありません。// U&& は転送参照です。 A(T&&, int*);// #2: T&& は転送参照ではありません。}; template<class T> A(T&&, int*)-> A<T>;// #3: T&& は転送参照です。 int i, *ip; A a{i, 0, ip};// エラー、 #1 から推定できません。 A a0{0, 0, ip};// #1 を使用して A<int> を推定し、 #1 を使用して初期化します。 A a2{i, ip};// #3 を使用して A<int&> を推定し、 #2 を使用して初期化します。
クラステンプレートを、そのクラステンプレートの特殊化である実引数1個から初期化するとき、一般的には、デフォルトでは、コピー推定候補がラッパーより優先されます。
std::tuple t1{1};// std::tuple<int>std::tuple t2{t1};// std::tuple<int> (std::tuple<std::tuple<int>> ではありません) std::vector v1{1, 2};// std::vector<int>std::vector v2{v1};// std::vector<int> (std::vector<std::vector<int>> ではありません) (P0702R1)std::vector v3{v1, v2};// std::vector<std::vector<int>>
コピー vs ラッパーの特別なケースを除けば、リスト初期化における初期化子リストコンストラクタの強い優先度はそのまま残されています。
std::vector v1{1, 2};// std::vector<int> std::vector v2(v1.begin(), v1.end());// std::vector<int>std::vector v3{v1.begin(), v1.end()};// std::vector<std::vector<int>::iterator>
クラステンプレートの実引数推定が導入される前は、実引数の明示的な指定を避けるための一般的な手法は、関数テンプレートを使うことでした。
std::tuple p1{1, 1.0};// std::tuple<int, double> (推定を使用)auto p2 =std::make_tuple(1, 1.0);// std::tuple<int, double> (C++17未満の手法)
[編集] 欠陥報告
以下の動作変更欠陥報告は以前に発行された C++ 標準に遡って適用されました。
DR | 適用先 | 発行時の動作 | 正しい動作 |
---|---|---|---|
P0702R1 | C++17 | an initializer-list constructor can pre-empt the copy deduction candidate, resulting in wrapping | initializer-list phase skipped when copying |