標籤:

類模板參數推導的注意事項(坑)

類模板參數推導的注意事項(坑)

31 人贊了文章

譯自 medium.com/@barryrevzin

在C++17之前,模板推導(基本上)只能用於兩種場合:函數模板的參數推導和變數類型/函數返回類型中的auto推導。過去無法推導類模板的模板參數。

結果就是:使用類模板的時候,只能(1)顯式指定模板參數,或(2)寫一個make_*輔助函數來做參數推導。對於(1),要麼是重複勞動/容易出錯(如果模板參數就是提供給構造函數的參數的類型),要麼根本是不可能的(如果參數是lambda)。對於(2),需要知道輔助函數到底叫什麼……輔助函數的名字並不總是符合make_*的模式。 標準庫有make_pairmake_tuplemake_move_iterator等符合模式的,但也有inserterback_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,明顯它不是類型——大家都知道它是類模板。但如果是自定義的類型,可能就不那麼明顯了。在上面的例子中,cd看上去都是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作為一個標註以確保xstd::tuple而不是其他東西(Concepts的目標)?

STL指出大多數程序員期望xy具有相同的含義。 但這樣的標註並非是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所指出的。ctuple<int>還是tuple<tuple<int>>zvector<int>還是vector<vector<int>>

現在,如果我們將CTAD用於複製,則複製優先。 這表示單參和多參實際上遵循不同的規則。現在,ctuple<int>zvector<int>。二者都只是複製構造自對應的參數。

換言之,如Casey所說,tuple(args...)的類型不僅取決於參數數量,還取決於參數類型。也就是說:

  • 如果sizeof...(args) != 1tuple<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。)


推薦閱讀:

如何理解C++的類和對象?

TAG:C17 | C | 編程語言 |