在上一次的文章中, 我們詳細介紹了 C++ 17 中引入的一個新的語法 : 省略樣板引數, 讓編碼器幫助我們推導. 其實不知道大家有沒有具體地去看 P0091R3, 裡面對於如何推斷引數為疊代器的情況進行了詳細介紹. 但是, 這並不足以解決全部的問題. 儘管在 P0702R1 中, 又提出了對 vector v {vector{1, 2}}; 推斷的修正

在我閱讀 Proposal 的時候, 我已經有一種隱約的感覺 : 當列表初始化和樣板引數推斷結合的時候, 會造成一些意想不到的問題

由於並沒有仔細深入去研究, 而且在提出編碼器去推斷樣板引數這個需求之後, 又有 P0512R0P0620R0 和 P0702R1, 所以我自然認為所以問題都已經被解決

直到遇到如下的程式碼 :

#include <iostream>

#include <vector>



using namespace std;

template <typename T>

void recursive_print(const vector<T> &vec) {

    if(vec.empty()) {

        return;

    }

    printf("%d\n", vec[0]);

    recursive_print(vector {vec.cbegin() + 1, vec.cend()});

}

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

    vector vec {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

    recursive_print(vec);

}

大家可以自己在自己的編碼器上嘗試一下, 我使用的是 Apple LLVM version 10.0.0 (clang-1000.11.45.5) 以及 gcc-8 (Homebrew GCC 8.2.0) 8.2.0. 在這兩個版本的編碼器上產生的最終結果都是編碼期無限迴圈, 我不得不手動終結編碼

如果將程式碼中的

printf("%d\n", vec[0]);

換為

cout << vec[0] << endl;

至少編碼器會給出函式無法配對的提示

出現無限迴圈的原因有兩個, 一個是我們並沒有給定樣板引數, 而選擇了讓編碼器幫助我們去推斷; 另外一個是我們使用了列表初始化. 但只有這兩個結合到一起的時候, 才會出現這樣的情況. 這是因為編碼器錯誤的樣板引數推斷. 我們來分析為什麼會有錯誤的推斷 :

在 C++ 引入列表初始化之後, C++ 的處理方式是更傾向於將初始化列表中給定的引數當作是初始化列表而優先配對

container(std::initializer_list<value_type>);

這個建構子, 而並非將初始化列表優先配對其它函式. 在這裡, 其實我們希望配對的建構子是 :

template <typename Iterator>

container(Iterator, Iterator);

因此,

recursive_print(vector {vec.cbegin() + 1, vec.cend()});

在第一次被推斷的時候, vector 的真實型別就變成了 :

vector<vector<int>::const_iterator>

在第二次推斷的時候, vector 的真實型別是 :

vector<vector<vector<int>::const_iterator>::const_iterator>

...

不斷這樣地推斷下去, 最終

vector<vector<vector<vector<vector<vector...

就造成了無法結束的迴圈

而編碼器並沒有那麼聰明地幫助我們結束這個無限迴圈的編碼或者給出一個編碼錯誤的提示, 而是陷入無限的迴圈中無法自拔...

在 P0091R3 中, 把

template <typename T>

struct iterator {

    using value_type = T;

    //...

}

這樣宣告別名的行為稱為 "detor", 並且採用如下的方式進行推斷

template <typename Iter>

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

但是對於列表初始化而言, 是優先去配對函式參數為 std::initializer_list<T> 的建構子, 所以自然不會用到上面的推斷標記, 因此推斷錯誤

針對上面的討論, 本人給閣下的建議是當遇到需要編碼器幫助我們推斷樣板引數並且結合列表初始化使用的時候, 我們應當 :

  • 放入合理的函式引數. 例如不管在什麼情況之下, 都只會和某一個建構子配對而不會和其它建構子配對
  • 明確給出樣板引數防止推斷錯誤