類模板參數推導的注意事項(坑)
31 人贊了文章
譯自 https://medium.com/@barryrevzin/quirks-in-class-template-argument-deduction-ad08af42ebd6
在C++17之前,模板推導(基本上)只能用於兩種場合:函數模板的參數推導和變數類型/函數返回類型中的auto
推導。過去無法推導類模板的模板參數。
結果就是:使用類模板的時候,只能(1)顯式指定模板參數,或(2)寫一個make_*
輔助函數來做參數推導。對於(1),要麼是重複勞動/容易出錯(如果模板參數就是提供給構造函數的參數的類型),要麼根本是不可能的(如果參數是lambda)。對於(2),需要知道輔助函數到底叫什麼……輔助函數的名字並不總是符合make_*
的模式。 標準庫有make_pair
、make_tuple
、make_move_iterator
等符合模式的,但也有inserter
、back_inserter
這樣不符合模式的。
類模板參數推導改變了這種情況,它允許根據類模板的構造函數或者根據推導指引(deduction guide)來推導類模板的參數。於是我們可以寫出這樣的代碼:
pair p(1, 2.0); // pair<int, double>tuple t(1, 2, 3.0); // tuple<int, int, double>template<class Func>class Foo() { public: Foo(Func f) : func(f) {} void operator()(int i) const { std::cout << "Calling with " << i << endl; f(i); } private: Func func; };for_each(vi.begin(), vi.end(), Foo([&](int i){...})); // Foo<some_lambda_type>
不需要顯式指定類型。不需要make_*
,即使參數是lambda。
然而,類模板參數推導(以下簡稱CTAD)有兩個值得注意的地方(坑)。
第一個地方是,頭一次出現了「兩個變數聲明看起來聲明同一個類型,實際上聲明不同類型」的情況(←_←其實不是頭一次)。
// 都使用auto,但我們並不期待它們對應同一個類型auto a = 1;auto b = 2.0;// 都使用std::pair,它看上去像一個類型但實際上卻不是// 兩個聲明對應不同的類型std::pair c(1, 2);std::pair d(1, 2.0);
我們用auto
的時候,都知道auto
不是一個類型。但是我們用類模板的名字的時候,需要停下來想一下。 當然,對於std::pair
,明顯它不是類型——大家都知道它是類模板。但如果是自定義的類型,可能就不那麼明顯了。在上面的例子中,c
和d
看上去都是std::pair
類型的對象——因此具有相同類型。但實際上它們分別是std::pair<int,int>
和std::pair<int,double>
類型的對象。
在C++20引入的Concepts有同樣的問題。實際上YAACD paper(《另一種有制約聲明的方法》)把CTAD作為支持Concept name = ...
的原因:
在變數聲明中,省略
auto
看來也是合理的:Constraint x = f2();
特別要注意的是,我們已經有一種語法可以進行(部分)推導,但是在語法中沒有明確體現推導:
std::tuple x = foo();
這種「使用看上去像類型但實際上不是類型的佔位符」的問題不會消失。恰恰相反,它會變得更加普遍。所以只要牢記這個問題就好了。
第二個注意事項(坑),對我來說,是一個更大的問題。它是Concepts和CTAD在意義上的不同之處,來自於CTAD試圖解決的問題。
在相關提案中,加入CTAD的動機可以概括成:我想構建一個類模板的實例(specialization),而不必顯式指定模板參數——只要自動推導,不需要我寫輔助工具或者查看這些參數是什麼。也就是說,我想構建出新東西。
加入Concepts的動機更廣,但對於有制約的變數聲明(constrained variable declaration)而言,動機是:我想構建一個對象,其類型我不關心,但我想要表達對這個類型的一系列要求,而不是直接使用auto
完事。也就是說,我仍然在使用已有的類型,只是加上了一個標註。
至少我(←_←本文的原作者)是這麼想的。
看起來這兩個想法不衝突,但實際上它們就是衝突的。看起來我們不需要在兩者之間做出選擇,但實際上需要。最近的Twitter討論串反映了這種衝突:
不要停下來啊,JF。
問題歸結為:這段代碼到底做了什麼:
std::tuple<int> foo();std::tuple x = foo();auto y = foo();
聲明變數x
的意圖是什麼?我們是在構建新東西(CTAD的目標)還是把std::tuple
作為一個標註以確保x
是std::tuple
而不是其他東西(Concepts的目標)?
STL指出大多數程序員期望x
和y
具有相同的含義。 但這樣的標註並非是CTAD的目標。CTAD是關於構建新東西的——這表示雖然y
顯然是std::tuple<int>
, 但x
應該是std::tuple<std::tuple<int>>
。畢竟,這就是我們所要求的。我們根據參數構建了一個新的類模板實例(class template specialization)。
在這個例子中,衝突表現得更加明顯:
// The tuple casestd::tuple a(1); // unquestionably, tuple<int>std::tuple b(a, a); // unquestionably, tuple<tuple<int>, tuple<int>>std::tuple c(a); // ??// The vector casestd::vector x{1}; // unquestionably, vector<int>std::vector y{x, x}; // unquestionably, vector<vector<int>>std::vector z{x}; // ??
這就是Casey所指出的。c
是tuple<int>
還是tuple<tuple<int>>
?z
是vector<int>
還是vector<vector<int>>
?
現在,如果我們將CTAD用於複製,則複製優先。 這表示單參和多參實際上遵循不同的規則。現在,c
是tuple<int>
,z
是vector<int>
。二者都只是複製構造自對應的參數。
換言之,如Casey所說,tuple(args...)
的類型不僅取決於參數數量,還取決於參數類型。也就是說:
- 如果
sizeof...(args) != 1
:tuple<decay_t<decltype(args)>...>
- 否則,如果
arg0
不是tuple
的實例:tuple<decay_t<decltype(arg0)>>
- 否則,
decay_t<decltype(arg0)>
這顯然不簡單。(←_←現實中的tuple
有5個推導指引,比這裡列出的更複雜)
我(←_←本文的原作者)認為這是一個不幸和不必要的衝突——尤其是考慮到Concepts即將到來。Concepts將使我們能夠輕鬆區分兩種情況:
template <typename T, template <typename...> class Z>concept Specializes = ...;// The tuple casetuple a(1); // unquestionably, tuple<int>tuple b(a, a); // unquestionably, tuple<tuple<int>, tuple<int>>tuple c(a); // tuple<tuple<int>>Specializes<tuple> d(a); // tuple<int>// The vector casevector x{1}; // unquestionably, vector<int>vector y{x, x}; // unquestionably, vector<vector<int>>vector z{x}; // vector<vector<int>>Specializes<vector> w{x}; // vector<int>
這樣,我們就能使每種語言特性做其最擅長的事:CTAD構建新東西,Concepts限制已有的東西。
但這就是現有的規則,因此記住這些坑是很重要的。尤其是第二個坑——這意味著在泛型代碼中使用CTAD時需要非常小心:
template <typename... Ts>auto make_vector(Ts... elems) { std::vector v{elems...}; assert(v.size() == sizeof...(elems)); // right?? return v;}auto a = make_vector(1, 2, 3); // okauto b = make_vector(1); // okauto c = make_vector(a, b); // okauto d = make_vector(c); // assert fires
(←_←雖然譯者覺得不會有人傻到這麼寫——即使沒有文中提到的坑,這寫法也有問題,比如make_vector(1u, 2, std::allocator<int>{});
就能造成assertion failure。)
推薦閱讀: