摘要訊息 : 見過不會停止的迴圈和不會停止的遞迴, 但是你是否見過不會停止的編碼?

0. 前言

《【C++ 17】為類別樣板推導樣板參數》中, 我們介紹了支援 C++ 17 的編碼器是如何為類別樣板推導省略掉的樣板參數的. 它主要基於提案 P0091R3. 事實上, C++ 17 有四篇關於省略類別樣板的提案, 除了 P0091R3 之外還有 P0512R0, P0620R0P0702R1. 這些提案都在細節上解決了省略樣板參數可能導致的問題. 例如, P0702R1 就解決了 std::vector v {std::vector {1, 2}}; 可能會被推導為 std::vector<std::vector<int>> 的問題 (正確的推導應該是使用複製建構子, 推導為 std::vector<int>). 然而, 省略類別樣板仍然會產生問題.

更新紀錄 :

  • 2022 年 4 月 17 日進行第一次更新和修正.

1. 問題程式碼

#include <cstdio>
#include <vector>

template <typename T>
void recursive_print(const std::vector<T> &vec) {
    if(vec.empty()) {
        return;
    }
    std::printf("%d", vec[0]);
    recursive_print(std::vector {vec.cbegin() + 1, vec.cend()});
}

int main(int argc, char *argv[]) {
    std::vector vec {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    recursive_print(vec);
}

我們來分析一下 Code 1. 在主函式 main 中, vec 被推導為 std::vector<int>, 然後它作為引數被傳遞給 recursive_print. 函式樣板 recursive_printT 被推導為 int. 在判斷 vec 是否為空之後, 由 std::printf 來輸出向量中第一個元素, 然後用剩餘元素建構一個新的 std::vector<int>, 遞迴地呼叫 recursive_print.

粗看當然很好, 但是在編碼的時候上面的程式碼會導致編碼器無限編碼, 也就是停不下來的編碼. 我們不得不手動終止. 在 Apple Clang 10.0.0 和 GCC 8.2.0 下都會出現這個問題.

2. 問題分析

我們在第 1 節中大致分析了一下 Code 1. 但是很顯然, 我們的分析存在錯誤, 否則編碼器的工作應該是正常的. 問題就出在遞迴呼叫自身的那一行程式碼上 recursive_print(std::vector {vec.cbegin() + 1, vec.cend()});.我們在《【C++ 17】為類別樣板推導樣板參數》中說過, 引數列表中 std::vector 的推導會使得編碼器可能產生

#include <iterator>
#include <vector>

template <typename Iterator>
std::vector<std::iterator_traits<Iterator>::value_type> __deduce_for_std_vector(Iterator, Iterator);

這樣的函式. 但是, 當物件的初始化使用的是初始化列表的時候, C++ 會優先呼叫參數為 std::initializer_list 的建構子. 在 recursive_print(std::vector {vec.cbegin() + 1, vec.cend()}); 中的 std::vector 的樣板引數是 std::vector<std::vector<int>::const_iterator>.

有人可能覺得奇怪, 對一個 C++ 疊代器使用 std::printf 輸出不會產生編碼錯誤嗎? 不會, 因為 std::printf 可以接受任意型別的引數. 如果我們把 std::printf 改用為 std::cout, 這裡就會產生函式無法匹配的錯誤.

但是即使這樣, 編碼器又怎麼會發生無限迴圈的編碼呢? 第一次呼叫 recursive_print, 編碼器推導的結果為 std::vector<std::vector<int>::const_iterator>. 遞迴地呼叫, 第二次推導的結果就是 std::vector<std::vector<std::vector<int>::const_iterator>>>. 接下來的遞迴, 樣板引數只會越來越長... 因此編碼器並不知道在哪裡停止, 所以只能一直推導下去, 最終的結果可能是記憶體耗盡.

3. 解決方案

對於這種可能產生問題地推導, 我們儘量不要讓編碼器進行. 取而代之的是, 我們應該自己明確類別樣板中的樣板引數.