標籤:

記 modern C++ 中的一個語法毒藥

20190425 更新:

媽耶,評論區的各位你們都冷靜一下。我當然知道這段代碼有明顯的問題。你看,它就是想列印向量中的所有元素,至於寫得這麼噁心嗎?但我就是拿它來說明問題的,不是來求助說:怎麼寫更好的。

Cpp11 以後就開始推廣花括弧的初始化方式,Cpp17 增加了模板參數推理更方便了。但是合一起就是毒藥了啊!退一萬步說,如果編譯報錯可能是代碼寫的有問題,但如果編譯器不報錯而是死循環死在那兒了,這還單純是代碼寫的不好的問題嗎?本質上是初始化列表的設計有問題。這語法把編譯器都「毒死了」哎!我想說的是這個,你們冷靜一下……

_____

首先看這麼一段代碼——不要吐槽它本身的實現,就是個示例代碼:

#include <stdio.h>
#include <vector>

template <typename T>
void printV(const std::vector<T>& v) {
if (v.size() == 1) {
printf("%d ", v[0]);
return;
} else if (v.empty()) {
return;
}
size_t middle = v.size() / 2;
printV(std::vector{v.begin(), v.begin() + middle});
printV(std::vector{v.begin() + middle, v.end()});
}

int main() {
std::vector<int> v = {1, 2, 3};
printV(v);
return 0;
}

你可能要問,std::vector{...} 這樣沒問題嗎?好了,這是 C++17 引入的新特性,名為模板參數推理(template argument deduction)。它可以在你不給出模板參數的情況下,推理出模板參數。具體來說,就是推理 std::vector<T> 中的 T

那這段代碼有什麼問題呢?——你編譯看看就好了,記得加上 -std=c++17 的 flag。編譯這段代碼,編譯器會在模板參數推理的過程中無限循環下去,然後你就可以感受風扇呼呼的魅麗了。

具體原因是啥咧?答案就在這麼個「語法毒藥(syntactic toxin)」上面:the strong preference for initializer-list constructors in list-initialization。也就是說,modern C++ 對列表初始化中的那一坨有強烈的把這一坨當成是一個初始化列表的傾向。於是乎, std::vector{v.begin(), v.begin()+ middle} 這句代碼,編譯器會構造一個含有 std::vector<int>::iterator 的向量,即:std::vector<std::vector<int>::iterator>。這樣下來,遞歸調用 printV 時,函數模板參數就變成了 std::vector<int>::iterator。再經歷一次類似的操作,函數模板參數就變成了 std::vector<std::vector<int>::iterator>::iterator。由於編譯器不知道遞歸調用何時停止,於是它會在編譯階段不停地重複這個過程,製造出一個無限循環的類型 std::vector<...<std::vector<std::vector<int>::iterator>::iterator>...>::iterator。這時候,你從外部觀察編譯器,就是編譯器死那兒了,CPU 使用率飆升,風扇呼呼地轉。

那麼如何評價 modern C++ 的這種特性(feature)呢?只能說是「語法毒藥(syntactic toxin)」了。


推薦閱讀:

TAG:C/C |