摘要訊息 : 見過不會停止的迴圈和不會停止的遞迴, 但是你是否見過不會停止的編碼?
0. 前言
在《【C++ 17】為類別樣板推導樣板參數》中, 我們介紹了支援 C++ 17 的編碼器是如何為類別樣板推導省略掉的樣板參數的. 它主要基於提案 P0091R3. 事實上, C++ 17 有四篇關於省略類別樣板的提案, 除了 P0091R3 之外還有 P0512R0, P0620R0 和 P0702R1. 這些提案都在細節上解決了省略樣板參數可能導致的問題. 例如, 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_print
的 T
被推導為 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. 解決方案
對於這種可能產生問題地推導, 我們儘量不要讓編碼器進行. 取而代之的是, 我們應該自己明確類別樣板中的樣板引數.
自創文章, 原著 : Jonny. 如若閣下需要轉發, 在已經授權的情況下請註明本文出處 :