摘要訊息 : C++ 17 可以省略類別樣板中的樣板引數了.
0. 前言
我們知道, 編碼器可以從函式的引數來推導函式樣板中的樣板參數, 但是編碼器卻不能從物件建構的過程中推導類別樣板的樣板參數 :
#include <initializer_list>
#include <vector>
template <typename T>
void f(std::initializer_list<T>);
int main(int argc, char *argv[]) {
f({1, 2, 3}); // OK
std::vector v {1, 2, 3}; // Error
}
其實這樣稍微有些不合理. 因為既然編碼器可以正常推導函式樣板 f
的樣板參數 T
為 int
, 那麼應該也可以從初始化列表中推導出 v
的樣板參數是 int
.
P0091R3 《Template argument deduction for class templates》提出, 從建構子的引數推導類別樣板的樣板參數.
更新紀錄 :
- 2022 年 4 月 16 日進行第一次更新和修正.
1. 提案內容
1.1 一般情形
為了可以讓編碼器可以從建構子接受的引數當中推導出樣板參數, 提案中準備了一個解決方案. 對於類別
template <typename T>
struct s {
s(const T *);
s(const T &);
};
我們可以寫出 s a {0};
或者 s b {(int *)0};
這樣的宣告, 編碼器為了推導省略掉的樣板引數, 會生成一些我們無法看見的多載函式 :
template <typename T>
struct s {
s(const T &);
s(const T *);
};
template <typename T>
s<T> __deduce_for_s(const s<T> &);
template <typename T>
s<T> __deduce_for_s(s<T> &&);
template <typename T>
s<T> __deduce_for_s(const T &);
template <typename T>
s<T> __deduce_for_s(const T *);
我們看見了明確的建構子引數和隱含的建構子引數. 當然, 名稱可能不太一樣. 產生這些函式之後, 對於 s a {0};
或者 s b {(int *)0};
, 編碼器會嘗試用建構子中的引數去匹配 __deduce_for_s
, 最終得到 T
的型別.
1.2 複雜情形
我們再來看一個比較複雜的類別 :
template <typename T>
struct outer {
template <typename U>
struct inner {
inner(T);
inner(T, U);
template <typename V>
inner(V, U);
};
};
對於宣告 outer<int>::inner a(2.0, 0);
, 編碼器可能會生成下面函式用於推導 :
template <typename T>
struct outer {
template <typename U>
struct inner {
inner(T);
inner(T, U);
template <typename V>
inner(V, U);
};
};
template <typename inner_U>
outer<int>::inner<inner_U> __deduce_for_outer_int_inner(int);
template <typename inner_U>
outer<int>::inner<inner_U> __deduce_for_outer_int_inner(int, inner_U);
template <typename inner_U, typename inner_V>
outer<int>::inner<inner_U> __deduce_for_outer_int_inner(inner_V, inner_U);
template <typename inner_U>
outer<int>::inner<inner_U> __deduce_for_outer_int_inner(const outer<int>::inner<inner_U> &);
template <typename inner_U>
outer<int>::inner<inner_U> __deduce_for_outer_int_inner(outer<int>::inner<inner_U> &&);
我們來觀察一下 a
的第一個引數顯然是 double
型別, 第二個引數是 int
型別. 這樣, 那些和 int
有關和建構有關的四個多載函式就會被排除, 只剩下第三個. 從而對於第三個函式, inner_U
被推導為 double
, inner_V
被推導為 int
. 這樣的話, inner
類別樣板的樣板參數 U
被推導為 double
, 帶有樣板的建構子的樣板參數 V
被推導為 int
. 最終, a
的型別被推導為 outer<int>::inner<int>
.
然而, 上面的推導被 P0091R3 給否定了. 帶有可視範圍運算子的樣板參數推導暫時不被支援, C++ 17 也確實不支援這樣做. 因此, 我們必須明確地給出 a
的型別, 包括樣板引數, 否則會產生編碼錯誤.
1.3 從疊代器建構
現在, 我們自己實作一個接受從疊代器建構的 vector
:
template <typename T>
class vector {
private:
T *begin;
std::size_t used;
std::size_t size;
public:
template <typename Iterator>
vector(Iterator first, Iterator last);
// ...
};
假如有宣告 vector v(begin, end);
, 其中 begin
和 end
是 int *
型別的指標, 那麼按照第 1.1 節的說法, 編碼器是無法為 v
推導樣板參數的. 因此針對疊代器這種情況, 編碼器還需要生成一個推導函式 :
#include <iterator>
template <typename Iterator>
vector<typename std::iterator_traits<Iterator>::value_type> __deduce_for_vector(Iterator, Iterator);
根據 SFINAE (【C++ Template Meta-Programming】SFINAE), 如果 Iterator
被推導為 int
這種不適合作為 std::iterator_traits
引數的型別, 那麼這個函式將會被忽略, 並不會產生編碼錯誤.
1.4 編碼器生成的幫助推導的函式
由於需要推導而生成的那些函式是編碼器專用的, 不會對外公開, 也不會跟外界的任何函式產生衝突, 甚至都不存在函式體.
另外, 編碼器不一定是按照上面這樣來生成函式的, 編碼器生成的可能只是一個標記 :
template <typename T, typename U>
__deduce_for_s(U) -> s<T>;
這並不是函式, 也不是宣告.
1.5 特殊情形
雖然編碼器支援從疊代器來推導樣板參數, 但是如果是在 return
陳述式中, 這樣的推導會失效 :
template <typename Iterator>
auto make_vector(Iterator begin, Iterator end) {
//return vector(begin, end); // Error
return vector<typename std::iterator_traits<Iterator>::value_type>(begin, end); // OK
}
對於參考來說, 可能會出現樣板參數的參考折疊問題 :
template <typename T>
struct s {
s(T &&);
};
如果我們明確給出 T
的型別, 那麼 s(T &&);
接受的是一個型別為 T
的右值參考, 但是如果我們讓編碼器來推導 T
的型別, 那麼 T &&
會被編碼器視為通用參考, 而不是右值參考. 這就會導致 T
會被推導為參考型別, 而不是普通型別. 一旦樣板參數是一個參考, 會帶來許多問題. 不過好在編碼器對這種情況也進行了特殊處理, 編碼器在推導的時候生成的函式借助了 std::remove_reference
:
template <typename T>
s<std::remove_reference_t<T>> __deduce_for_s(T &&);
2. 評論
其實這篇提案很多都是針對編碼器的推導進行討論的. 對於一般的 C++ 程式設計師來說, 編碼器的細節他們根本不需要考慮. 但是如果閣下想要成為一個資深的 C++ 程式設計師, 那麼這些細節是必須要了解的, 因為資深的 C++ 程式設計師肯定需要了解哪些情況下編碼器才會成功推導類別樣板的樣板參數.
自創文章, 原著 : Jonny. 如若閣下需要轉發, 在已經授權的情況下請註明本文出處 :