摘要訊息 : C++ 17 Proposal P0091R3 《Template argument deduction for class templates》導讀

C++ 17 Proposal P0091R3 《Template argument deduction for class templates》

在 C++ 11 中引入了 auto 以及 decltype 兩個關鍵字, 它們都是用來推斷一個型別的. 儘管如此, 在某些場景之下, 明確標明樣板參數還是顯得非常累贅 :

#include <vector>

#include <functional>

#include <mutex>

#include <shared_mutex>



using namespace std;



template <typename F>

struct Foo {

    Foo(F) {}

};



vector<void **> func(void *, unsigned, reference_wrapper<int>);



int main(int argc, char *argv[]) {

    vector<int> vec {1, 2, 3};

    mutex m;

    unique_lock<decltype(m)> ul(m, defer_lock);

    //若沒有 decltype 和 auto

    Foo<vector<void **>(void *, unsigned, reference_wrapper<int>)> f {func};

    //即使有 decltype 和 auto

    Foo<decltype(func)> f2 {func};

    auto f3 {func};     //f3 的型別會被推斷為 vector<void **>(void *, unsigned, reference_wrapper<int>), 而不是 Foo<vector<void **>(void *, unsigned, reference_wrapper<int>>)

    auto f4 {Foo<decltype(func)> {func}};

}

對於上述程式碼, 我們希望可以省略 vectorfunction 的樣板引數, 因為它們可以從給定的建構子引數中推斷 :

#include <vector>

#include <functional>

#include <mutex>

#include <shared_mutex>



using namespace std;



template <typename F>

struct Foo {

    Foo(F) {}

};



vector<void **> func(void *, unsigned, reference_wrapper<int>);



int main(int argc, char *argv[]) {

    vector vec {1, 2, 3};

    mutex m;

    unique_lock ul(m, defer_lock);

    Foo f {func};

}

相比起第一個程式碼, 第二個看起來就清爽多了

而對於第一個程式碼而言, 這些樣板參數確實可有可無, 這就好比 :

template <typename T>

void func(T) {}



int main(int argc, char *argv[]) {

    func(0);        //T => int

    func<int>(0);       //沒有必要

}

在第二次呼叫 func 函式的時候, 我們明確寫出 T 的型別, 但是這個是沒有必要的

對於類別樣板來說, 也是如此. 所以, 樣板引數為什麼就不能像函式樣板引數一樣讓編碼器為我們推斷它呢?

所以這個 Proposal 就提出了讓編碼器幫我們推斷類別樣板的樣板引數, 但是編碼器到底是如何進行推斷的呢? Proposal 裡面提出了一種方案

對於一個普通的類別而言, 例如 vector, 編碼器對於它們的推斷會自動產生一些我們不可見的多載函式集合

假設我們現在有這樣的類別 :

template <typename T>

struct Foo {

    Foo(T);

    Foo(T *);

    Foo(T &);

};

用戶可以直接寫出如下的程式碼 :

Foo f {0};

Foo f2 {(int *)0};

編碼器在推斷 f 和 f2 具體的型別時, 會產生如下的多載函式集合 :

template <typename T>

Foo<T> f(const Foo<T> &);        //#1

template <typename T>

Foo<T> f(Foo<T> &&);        //#2

template <typename T>

Foo<T> f(T);        //#3

template <typename T>

Foo<T> f(T *);        //#4

我們可以看到, 其中包括了明確的建構子和隱含的建構子 (複製建構子和移動建構子)

在產生這些函式之後, 編碼器會通過類似於函式呼叫的方式去對每個函式進行匹配

最終, Foo f {0} 可以匹配的函式是第三個函式, T 型別為 int; Foo f {(int *)0} 可以匹配的函式是第四個函式, T 型別為 int

我們再來看一個稍微複雜的示例, 這個示例來自於 Proposal :

template <typename T> struct Foo {

    template <typename U> struct Bar {

        Bar(T);

        Bar(T, U);

        template <typename V>

        Bar(V, U);

    };

};

Foo<int>::Bar b(2.0, 0);

我們幾乎可以肯定, 編碼器會為 Foo<T>::Bar 創建一些多載函式集合. 這其中包裹了所有的用戶自訂建構子、複製建構子以及移動建構子 :

template <typename U>

Foo<int>::Bar<U> f(const Foo<int>::Bar<U> &);        //#1

template <typename U>

Foo<int>::Bar<U> f(Foo<int>::Bar<U> &&);        //#2

template <typename U>

Foo<int>::Bar<U> f(int);        //#3

template <typename U>

Foo<int>::Bar<U> f(int, U);        //#4

template <typename U, typename V>

Foo<int>::Bar<U> f(V, U);        //#5

首先觀察變數 b 的第一個引數 2.0, 它預設是一個 double 型別, 並且此處沒有型別轉型, 所以前面四個多載函式直接會被忽略, 那麼就只剩下最後一個可選了. 不過, 我們還是需要看看第二個引數 0, 它預設是一個 int 型別, 並且也是不存在任何型別轉型, 那麼最終具現化的函式是

template <>

Foo<int>::Bar<double, int> f(double, int);

也就是說, 變數 b 的型別是 Foo<int>::Bar<int>

在這個示例裡, 你需要注意的是, 上述程式碼並不可以通過編碼器的編碼. 因為目前為止, 帶有可視範圍運算子的類別樣板還未在可推導的範圍之內

上面對於樣板引數的推導都是比較明顯的, 然而對於某些建構子而言, 上面的推導無法直接進行 :

template <typename T, typename Allocator>

class vector {

    //...

public:

    template <typename Iter>

    vector(Iter, Iter);

};

如果閣下可以看懂之前的部分, 我想你非常清楚這裡 vector 的型別為 vector<typename iterator_traits<Iter>::value_type> 而不是 vector<Iter>

在這個時候, 編碼器會使用用戶自訂推導指引, 即編碼器會生成類似於函式的程式碼進行推導 :

template <typename Iter>

vector(Iter, Iter) -> vector<typename iterator_traits<Iter>::value_type>;

我們可以發現, 上述程式碼是先進行了 Iter 的匹配, 然後推導 vector 的樣板引數

雖然程式碼非常類似於函式的宣告, 但是它並不會成為可視範圍之內的函式, 而且函式匹配時也不會出現這樣的指引. 初次之外, 它是無體的

需要注意的是, 函式樣板體內你需要明確告訴編碼器, 具體的樣板引數是什麼 :

template <typename Iter>

auto make_vec(Iter b, Iter e) {

    return vector(b, e);

}

例如上面的程式碼在使用的時候就會產生編碼錯誤, 編碼器無法直接推斷 vector 的樣板引數, 需要修改成下面的形式 :

template <typename Iter>

auto make_vec(Iter b, Iter e) {

    return vector<typename iterator_traits<Iter>::value_type>(b, e);

}

對於參考來說, 可能會出現樣板參數的參考折疊問題 :

template <typename T>

class Foo {

    T t;

    public:

        Foo(const T &t) : t {t} {}

        Foo(T &&t) : t {move(t)} {}

};

在上述程式碼中, 編碼器會視 T && 為一個帶參考折疊的通用參考而並非是一個 T 的右值參考. 在樣板引數推斷的時候, 雖然不會引發運作期的問題, 但是可能會造成編碼錯誤 (樣板引數中放置參考對於很多類別來說都可能產生意想不到的編碼錯誤, 所以我們通常放入 reference_wrapper 來替代直接放入參考). 所以在對待帶參考折疊的通用參考時, 編碼器會產生一個特殊的明確推斷導引 :

template <typename T>

Foo(T &&) -> Foo<remove_reference_t<T>>

這樣就可以避免 T 被編碼器推斷為 T 的參考, 從而避免了一些不必要的問題

 

眾所周知, C++ 一直被認為是一門非常複雜的程式設計語言. 但是 C++ 的愛好者們也不斷地為簡化 C++ 作出了不少努力. 從 C++ 11 的 auto、Range-For、decltype 到 C++ 17 的讓編碼器推斷類別樣板的樣板引數. C++ 的程式設計師們只要可以用上最新的 C++ 來編寫程式碼, 那他們的工作量可以不斷地減少, 這是大家都可以看到的

這篇 Proposal 可以說並沒有帶來新的內容 (對於客端而言, 你可能根本不需要學習編碼器是如何推斷樣板引數的), 但它卻非常有用, 因為它讓 C++ 程式碼的編寫更加快速