名稱空間
變體
操作

類模板引數推導 (CTAD) (C++17 起)

來自 cppreference.com
< cpp‎ | 語言
 
 
C++ 語言
 
 
 
 

為了例項化一個類模板,每個模板實參都必須是已知的,但並非每個模板實參都必須明確指定。在以下語境中,編譯器將從初始化器型別推導模板實參:

  • 任何指定變數和變數模板初始化的宣告,其宣告型別是類模板(可能cv 限定
std::pair p(2, 4.5);     // deduces to std::pair<int, double> p(2, 4.5);
std::tuple t(4, 3, 2.5); // same as auto t = std::make_tuple(4, 3, 2.5);
std::less l;             // same as std::less<void> l;
template<class T>
struct A
{
    A(T, T);
};
 
auto y = new A{1, 2}; // allocated type is A<int>
auto lck = std::lock_guard(mtx);     // deduces to std::lock_guard<std::mutex>
std::copy_n(vi1, 3,
    std::back_insert_iterator(vi2)); // deduces to std::back_insert_iterator<T>,
                                     // where T is the type of the container vi2
std::for_each(vi.begin(), vi.end(),
    Foo([&](int i) {...}));          // deduces to Foo<T>,
                                     // where T is the unique lambda type
template<class T>
struct X
{
    constexpr X(T) {}
};
 
template<X x>
struct Y {};
 
Y<0> y; // OK, Y<X<int>(0)>
(C++20 起)

目錄

[編輯] 類模板的推導

[編輯] 隱式生成的推導指引

當在函式式轉換或變數宣告中,型別說明符僅由主類模板C的名稱組成(即沒有附帶的模板實參列表)時,將按如下方式形成推導候選:

  • 如果C已定義,對於在命名主模板中宣告的每個建構函式(或建構函式模板)Ci,構造一個虛構的函式模板Fi,使其滿足以下所有條件:
  • Fi的模板引數是C的模板引數,後面跟著(如果Ci是建構函式模板)Ci的模板引數(也包括預設模板引數)。
  • Fi關聯約束C的關聯約束和Ci的關聯約束的合取。
(C++20 起)
  • Fi引數列表Ci的引數列表。
  • Fi的返回型別是C,後面是類模板的模板引數,用<>括起來。
  • 如果C未定義或未宣告任何建構函式,則新增一個額外的虛構函式模板,如上所述從一個假想的建構函式C()派生。
  • 無論如何,新增一個額外的虛構函式模板,如上所述從一個假想的建構函式C(C)派生,稱為複製推導候選。
  • Fi的引數列表是Gi的引數列表。
  • Fi的返回型別是Gi的簡單模板識別符號。
  • 如果Gi有模板引數(語法(2)),則Fi是一個函式模板,其模板引數列表是Gi的模板引數列表。否則,Fi是一個函式。
  • 此外,如果
  • C已定義並滿足聚合型別的要求,假設任何依賴基類沒有虛擬函式或虛基類,
  • C沒有使用者定義的推導指引,並且
  • 變數使用非空初始化器列表arg1, arg2, ..., argn(可以使用指定初始化器)初始化,
可以新增一個聚合推導候選。聚合推導候選的引數列表由聚合元素型別生成,如下所示:
  • ei是從argi初始化的(可能遞迴的)聚合元素,其中
  • 對於任何具有以下特徵的聚合元素,不考慮花括號省略
  • 如果C(或其自身是聚合的元素)有一個基類是包展開
  • 如果包展開是尾隨聚合元素,則它被認為匹配初始化器列表的所有剩餘元素;
  • 否則,該包被認為是空的。
  • 如果沒有這樣的ei,則不新增聚合推導候選。
  • 否則,確定聚合推導候選的引數列表T1, T2, ..., Tn,如下所示:
  • 如果ei是陣列且argi帶花括號的初始化列表,則Ti是對ei的宣告型別的右值引用。
  • 如果ei是陣列且argi字串字面量,則Ti是對ei的const限定宣告型別的左值引用。
  • 否則,Tiei的宣告型別。
  • 如果因為包是非尾隨聚合元素而被跳過,則在其原始聚合元素位置插入一個形式為Pj ...的額外引數包。(這通常會導致推導失敗。)
  • 如果包是尾隨聚合元素,則與它對應的尾隨引數序列被替換為一個形式為Tn ...的單一引數。
聚合推導候選是如上所述從假想建構函式C(T1, T2, ..., Tn)派生的虛構函式模板。
在聚合推導候選的模板引數推導過程中,尾隨引數包中的元素數量僅在其未以其他方式推導時,才從剩餘函式引數的數量中推導。
template<class T>
struct A
{
    T t;
 
    struct
    {
        long a, b;
    } u;
};
 
A a{1, 2, 3};
// aggregate deduction candidate:
//   template<class T>
//   A<T> F(T, long, long);
 
template<class... Args>
struct B : std::tuple<Args...>, Args... {};
 
B b{std::tuple<std::any, std::string>{}, std::any{}};
// aggregate deduction candidate:
//   template<class... Args>
//   B<Args...> F(std::tuple<Args...>, Args...);
 
// type of b is deduced as B<std::any, std::string>
(C++20 起)

然後執行模板引數推導過載決議,以初始化一個假想類型別的虛構物件,其建構函式簽名與指引匹配(除了返回型別),以形成過載集,並且初始化器由執行類模板引數推導的語境提供,但如果初始化器列表包含單個型別(可能cv限定)U的表示式,其中UC的特化或從C的特化派生的類,則省略列表初始化的第一階段(考慮初始化器列表建構函式)。

這些虛構的建構函式是假想類型別的公有成員。如果指引是由顯式建構函式形成,或指引被宣告為explicit,則它們是顯式的。如果過載決議失敗,程式將格式錯誤。否則,所選的F模板特化的返回型別成為推匯出的類模板特化。

template<class T>
struct UniquePtr
{
    UniquePtr(T* t);
};
 
UniquePtr dp{new auto(2.0)};
 
// One declared constructor:
// C1: UniquePtr(T*);
 
// Set of implicitly-generated deduction guides:
 
// F1: template<class T>
//     UniquePtr<T> F(T* p);
 
// F2: template<class T> 
//     UniquePtr<T> F(UniquePtr<T>); // copy deduction candidate
 
// imaginary class to initialize:
// struct X
// {
//     template<class T>
//     X(T* p);         // from F1
//     
//     template<class T>
//     X(UniquePtr<T>); // from F2
// };
 
// direct-initialization of an X object
// with "new double(2.0)" as the initializer
// selects the constructor that corresponds to the guide F1 with T = double
// For F1 with T=double, the return type is UniquePtr<double>
 
// result:
// 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};
 
// the implicitly-generated deduction guides are (note that T is already known to be 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>); (copy deduction candidate)
 
// Overload resolution for direct-list-init with "{2.0, 1}" as the initializer
// chooses F3 with U=int and V=double.
// The return type is S<int>::N<int>
 
// result:
// S<int>::N<int> x{2.0, 1};

[編輯] 使用者定義的推導指引

使用者定義的推導指引的語法是函式(模板)宣告的語法,帶有一個尾隨返回型別,但它使用類模板的名稱作為函式名

explicit(可選) template-name ( parameter-list ) -> simple-template-id requires-clause(可選) ; (1)
template <template-parameter-list> requires-clause(可選)
explicit(可選) template-name ( parameter-list ) -> simple-template-id requires-clause(可選) ;
(2)
template-parameter-list - 一個非空的逗號分隔的模板引數列表
explicit - 一個explicit說明符
template-name - 其引數將被推導的類模板的名稱
parameter-list - 一個(可能為空的)引數列表
simple-template-id - 一個簡單模板識別符號
requires-clause - (C++20 起) 一個requires子句


使用者定義的推導指引的引數不能有佔位符型別:不允許使用縮寫函式模板語法。

(C++20 起)

使用者定義的推導指引必須命名一個類模板,並且必須在與類模板相同的語義作用域(可以是名稱空間或包含類)內引入,並且對於成員類模板,必須具有相同的訪問許可權,但推導指引不會成為該作用域的成員。

推導指引不是函式,也沒有函式體。推導指引不透過名稱查詢找到,除了在推導類模板引數時與其他推導指引進行過載決議外,不參與過載決議。推導指引不能在同一個翻譯單元中為同一個類模板重新宣告。

// declaration of the template
template<class T>
struct container
{
    container(T t) {}
 
    template<class Iter>
    container(Iter beg, Iter end);
};
 
// additional deduction guide
template<class Iter>
container(Iter b, Iter e) -> container<typename std::iterator_traits<Iter>::value_type>;
 
// uses
container c(7); // OK: deduces T=int using an implicitly-generated guide
std::vector<double> v = {/* ... */};
auto d = container(v.begin(), v.end()); // OK: deduces T=double
container e{5, 6}; // Error: there is no std::iterator_traits<int>::value_type

為了過載決議目的的虛構建構函式(如上所述)是顯式的,如果它們對應於由顯式建構函式形成的隱式生成的推導指引,或者對應於被宣告為explicit的使用者定義的推導指引。與往常一樣,在複製初始化語境中會忽略這些建構函式。

template<class T>
struct A
{
    explicit A(const T&, ...) noexcept; // #1
    A(T&&, ...);                        // #2
};
 
int i;
A a1 = {i, i}; // error: cannot deduce from rvalue reference in #2,
               // and #1 is explicit, and not considered in copy-initialization.
A a2{i, i};    // OK, #1 deduces to A<int> and also initializes
A a3{0, i};    // OK, #2 deduces to A<int> and also initializes
A a4 = {0, i}; // OK, #2 deduces to A<int> and also initializes
 
template<class T>
A(const T&, const T&) -> A<T&>; // #3
 
template<class T>
explicit A(T&&, T&&)  -> A<T>;  // #4
 
A a5 = {0, 1}; // error: #3 deduces to A<int&>
               // and #1 & #2 result in same parameter constructors.
A a6{0, 1};    // OK, #4 deduces to A<int> and #2 initializes
A a7 = {0, i}; // error: #3 deduces to A<int&>
A a8{0, i};    // error: #3 deduces to A<int&>
 
// Note: check https://github.com/cplusplus/CWG/issues/647, claiming that
// examples a7 and a8 are incorrect, to be possibly replaced as
//A a7 = {0, i}; // error: #2 and #3 both match, overload resolution fails
//A a8{i,i};     // error: #3 deduces to A<int&>,
//               //        #1 and #2 declare same constructor

在建構函式或建構函式模板的引數列表中使用成員 typedef 或別名模板本身不會使隱式生成的指引的相應引數成為非推導語境。

template<class T>
struct B
{
    template<class U>
    using TA = T;
 
    template<class U>
    B(U, TA<U>); // #1
};
 
// Implicit deduction guide generated from #1 is the equivalent of
//     template<class T, class U>
//     B(U, T) -> B<T>;
// rather than
//     template<class T, class U>
//     B(U, typename B<T>::template TA<U>) -> B<T>;
// which would not have been deducible
 
B b{(int*)0, (char*)0}; // OK, deduces B<char*>

別名模板的推導

當函式式轉換或變數宣告使用別名模板A的名稱作為型別說明符,而不帶引數列表時,其中A定義為B<ArgList>的別名,B的作用域是非依賴的,並且B是類模板或類似定義的別名模板,推導將以與類模板相同的方式進行,不同之處在於指引是從B的指引生成的,如下所示:

  • 對於B的每個指引f,使用模板引數推導B<ArgList>推導f的返回型別的模板引數,但即使某些引數未推導成功,推導也不會失敗。如果由於其他原因推導失敗,則使用一組空的推導模板引數繼續。
  • 將上述推導結果替換到f中,如果替換失敗,則不產生指引;否則,令g表示替換結果,形成一個指引f',使其滿足:
  • f'的引數型別和返回型別與g相同
  • 如果f是模板,則f'是函式模板,其模板引數列表由A的所有模板引數(包括其預設模板實參)組成,這些引數出現在上述推導中或(遞迴地)在其預設模板實參中,然後是f中未推導的模板引數(包括其預設模板實參);否則(f不是模板),f'是一個函式
  • f'的關聯約束g的關聯約束的合取以及一個約束,該約束當且僅當A的引數可以從結果型別中推導時才滿足
template<class T>
class unique_ptr
{
    /* ... */
};
 
template<class T>
class unique_ptr<T[]>
{
    /* ... */
};
 
template<class T>
unique_ptr(T*) -> unique_ptr<T>;   // #1
 
template<class T>
unique_ptr(T*) -> unique_ptr<T[]>; // #2
 
template<class T>
concept NonArray = !std::is_array_v<T>;
 
template<NonArray A>
using unique_ptr_nonarray = unique_ptr<A>;
 
template<class A>
using unique_ptr_array = unique_ptr<A[]>;
 
// generated guide for unique_ptr_nonarray:
 
// from #1 (deduction of unique_ptr<T> from unique_ptr<A> yields T = A):
// template<class A>
//     requires(argument_of_unique_ptr_nonarray_is_deducible_from<unique_ptr<A>>)
// auto F(A*) -> unique_ptr<A>;
 
// from #2 (deduction of unique_ptr<T[]> from unique_ptr<A> yields nothing):
// template<class T>
//     requires(argument_of_unique_ptr_nonarray_is_deducible_from<unique_ptr<T[]>>)
// auto F(T*) -> unique_ptr<T[]>;
 
// where argument_of_unique_ptr_nonarray_is_deducible_from can be defined as
 
// template<class>
// class AA;
 
// template<NonArray A>
// class AA<unique_ptr_nonarray<A>> {};
 
// template<class T>
// concept argument_of_unique_ptr_nonarray_is_deducible_from =
//     requires { sizeof(AA<T>); };
 
// generated guide for unique_ptr_array:
 
// from #1 (deduction of unique_ptr<T> from unique_ptr<A[]> yields T = A[]):
// template<class A>
//     requires(argument_of_unique_ptr_array_is_deducible_from<unique_ptr<A[]>>)
// auto F(A(*)[]) -> unique_ptr<A[]>;
 
// from #2 (deduction of unique_ptr<T[]> from unique_ptr<A[]> yields T = A):
// template<class A>
//     requires(argument_of_unique_ptr_array_is_deducible_from<unique_ptr<A[]>>)
// auto F(A*) -> unique_ptr<A[]>;
 
// where argument_of_unique_ptr_array_is_deducible_from can be defined as
 
// template<class>
// class BB;
 
// template<class A>
// class BB<unique_ptr_array<A>> {};
 
// template<class T>
// concept argument_of_unique_ptr_array_is_deducible_from =
//     requires { sizeof(BB<T>); };
 
// Use:
unique_ptr_nonarray p(new int); // deduced to unique_ptr<int>
// deduction guide generated from #1 returns unique_ptr<int>
// deduction guide generated from #2 returns unique_ptr<int[]>, which is ignored because
//   argument_of_unique_ptr_nonarray_is_deducible_from<unique_ptr<int[]>> is unsatisfied
 
unique_ptr_array q(new int[42]); // deduced to unique_ptr<int[]>
// deduction guide generated from #1 fails (cannot deduce A in A(*)[] from new int[42])
// deduction guide generated from #2 returns unique_ptr<int[]>
(C++20 起)

[編輯] 注意

類模板引數推導僅在不存在模板引數列表時執行。如果指定了模板引數列表,則不進行推導。

std::tuple t1(1, 2, 3);                // OK: deduction
std::tuple<int, int, int> t2(1, 2, 3); // OK: all arguments are provided
 
std::tuple<> t3(1, 2, 3);    // Error: no matching constructor in tuple<>.
                             //        No deduction performed.
std::tuple<int> t4(1, 2, 3); // Error

聚合的類模板引數推導通常需要使用者定義的推導指引

template<class A, class B>
struct Agg
{
    A a;
    B b;
};
// implicitly-generated guides are formed from default, copy, and move constructors
 
template<class A, class B>
Agg(A a, B b) -> Agg<A, B>;
// ^ This deduction guide can be implicitly generated in C++20
 
Agg agg{1, 2.0}; // deduced to Agg<int, double> from the user-defined guide
 
template<class... T>
array(T&&... t) -> array<std::common_type_t<T...>, sizeof...(T)>;
auto a = array{1, 2, 5u}; // deduced to array<unsigned, 3> from the user-defined guide
(C++20 前)

使用者定義的推導指引不必是模板

template<class T>
struct S
{
    S(T);
};
S(char const*) -> S<std::string>;
 
S s{"hello"}; // deduced to 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); // no deduction: X is the current X<T>
    }
 
    template<class Iter>
    auto bar(Iter b, Iter e)
    {
        return X<typename Iter::value_type>(b, e); // must specify what we want
    }
 
    auto baz()
    {
        return ::X(0); // not the injected-class-name; deduced to be X<int>
    }
};

過載決議中,偏序優先於函式模板是否由使用者定義的推導指引生成:如果由建構函式生成的函式模板比由使用者定義的推導指引生成的更特化,則選擇由建構函式生成的。因為複製推導候選通常比包裝建構函式更特化,所以此規則意味著通常優先選擇複製而不是包裝。

template<class T>
struct A
{
    A(T, int*);     // #1
    A(A<T>&, int*); // #2
 
    enum { value };
};
 
template<class T, int N = T::value>
A(T&&, int*) -> A<T>; //#3
 
A a{1, 0}; // uses #1 to deduce A<int> and initializes with #1
A b{a, 0}; // uses #2 (more specialized than #3) to deduce A<int> and initializes with #2

當較早的決勝規則(包括偏序)未能區分兩個候選函式模板時,適用以下規則:

  • 由使用者定義的推導指引生成的函式模板優先於由建構函式或建構函式模板隱式生成的函式模板。
  • 複製推導候選優先於所有其他由建構函式或建構函式模板隱式生成的函式模板。
  • 由非模板建構函式隱式生成的函式模板優先於由建構函式模板隱式生成的函式模板。
template<class T>
struct A
{
    using value_type = T;
 
    A(value_type); // #1
    A(const A&);   // #2
    A(T, T, int);  // #3
 
    template<class U>
    A(int, T, U);  // #4
};                 // #5, the copy deduction candidate A(A);
 
A x(1, 2, 3); // uses #3, generated from a non-template constructor
 
template<class T>
A(T) -> A<T>; // #6, less specialized than #5
 
A a(42); // uses #6 to deduce A<int> and #1 to initialize
A b = a; // uses #5 to deduce A<int> and #2 to initialize
 
template<class T>
A(A<T>) -> A<A<T>>; // #7, as specialized as #5
 
A b2 = a; // uses #7 to deduce A<A<int>> and #1 to initialize

指向cv不限定模板引數的右值引用不是轉發引用,如果該引數是類模板引數

template<class T>
struct A
{
    template<class U>
    A(T&&, U&&, int*); // #1: T&& is not a forwarding reference
                       //     U&& is a forwarding reference
 
    A(T&&, int*);      // #2: T&& is not a forwarding reference
};
 
template<class T>
A(T&&, int*) -> A<T>; // #3: T&& is a forwarding reference
 
int i, *ip;
A a{i, 0, ip};  // error, cannot deduce from #1
A a0{0, 0, ip}; // uses #1 to deduce A<int> and #1 to initialize
A a2{i, ip};    // uses #3 to deduce A<int&> and #2 to initialize

當從一個與所討論的類模板特化型別相同的單個引數進行初始化時,預設情況下通常優先選擇複製推導而不是包裝

std::tuple t1{1};  //std::tuple<int>
std::tuple t2{t1}; //std::tuple<int>, not std::tuple<std::tuple<int>>
 
std::vector v1{1, 2};   // std::vector<int>
std::vector v2{v1};     // std::vector<int>, not std::vector<std::vector<int>> (P0702R1)
std::vector v3{v1, v2}; // std::vector<std::vector<int>>

除了複製與包裝的特殊情況外,初始化器列表建構函式在列表初始化中的強烈偏好保持不變。

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>, using deduction
auto p2 = std::make_tuple(1, 1.0); //std::tuple<int, double>, pre-C++17
功能測試宏 標準 特性
__cpp_deduction_guides 201703L (C++17) 類模板的模板引數推導
201907L (C++20) 聚合和別名的CTAD

[編輯] 缺陷報告

下列更改行為的缺陷報告追溯地應用於以前出版的 C++ 標準。

缺陷報告 應用於 釋出時的行為 正確的行為
CWG 2376 C++17 即使宣告的變數型別不同於其引數將被推導的類模板,也會執行CTAD
當宣告的變數型別與要推導引數的類模板不同時
不執行
CTAD
CWG 2628 C++20 隱式推導指引未傳播約束 傳播約束
CWG 2697 C++20 不清楚是否允許在使用者定義的推導指引中使用縮寫函式模板語法
使用者定義的推導指引不允許使用縮寫函式模板語法
已禁止
CWG 2707 C++20 推導指引不能有尾隨的requires子句 推導指引可以有尾隨的requires子句
CWG 2714 C++17 隱式推導指引未考慮建構函式的預設引數
考慮建構函式的預設引數
考慮它們
CWG 2913 C++20 CWG 問題 2707 的解決方案使得推導指引語法與函式宣告語法不一致
調整了語法
調整了語法
P0702R1 C++17 初始化器列表建構函式可以搶佔複製推導候選,導致包裝
初始化器列表階段
複製時跳過
複製時跳過