C++ 的多型除了體現在類別之外, 還體現於函式的多載. 函式的多載屬於編碼期多型, 也就是靜態多型. 在上一篇文章中, 我們講述了 Traits 技巧, 於是不難想到, 我們可以用這個疊代器標記來對應的不同函式和不同行為 :
#include <iostream>
using namespace std;
template <typename InputIterator>
void func(InputIterator, InputIterator, input_iterator_tag) {
cout << "input iterator" << endl;
}
template <typename OutputIterator>
void func(OutputIterator, OutputIterator, output_iterator_tag) {
cout << "output iterator" << endl;
}
template <typename ForwardIterator>
void func(ForwardIterator, ForwardIterator, forward_iterator_tag) {
cout << "forward iterator" << endl;
}
template <typename BidirectionalIterator>
void func(BidirectionalIterator, BidirectionalIterator, bidirectional_iterator_tag) {
cout << "bidirectional iterator" << endl;
}
template <typename RandomAccessIterator>
void func(RandomAccessIterator, RandomAccessIterator, random_access_iterator_tag) {
cout << "random access iterator" << endl;
}
int main(int argc, char *argv[]) {
func(reinterpret_cast<int *>(0), reinterpret_cast<int *>(0), typename iterator_traits<int *>::iterator_category {}); //輸出 : random access iterator
}
對於高層的程式庫使用者來說, 每一個函式後面都加一個 typename iterator_traits<iterator>::iterator_category()
顯然是過於累贅的, 因為每個疊代器都會有自己的疊代器類型, 而且這些都是編碼期可預知的, 因此我們自然想去掉這個看似多餘的函式參數
參閱標準程式庫文檔之後, 我們發現, 標準庫的所有容器, 它們的建構子都支援放入一個範圍疊代器, 但是它們不需要明確地給出疊代器標籤, 因此我們的猜想是可行的. 對於如何做到函式參數無需放入疊代器標籤, 我們有幾個初步的想法 :
首先, 我們嘗試在函式內部限制疊代器. 如果不想在運作期有任何的消耗, 那麼我們可以使用 using
宣告一個疊代器標籤的別名 :
template <typename InputIterator>
void func(InputIterator begin, InputIterator end) {
using iterator_category = typename iterator_traits<InputIterator>::iterator_category;
}
int main(int argc, char *argv[]) {
func(static_cast<int *>(nullptr), static_cast<int *>(nullptr)); //OK
func(0, 0); //Error : no type named 'iterator_category' in 'std::__1::iterator_traits<int>'
}
看起來貌似可行, 但是實際上我們只是限定了它必須是一個疊代器, 如若我們要更加細緻地限定疊代器的具體種類, 那麼它束手無策, 因此我們要想其它的方案
我們曾在這個系列的第一篇文章中介紹過超函式, 要想判斷一個疊代器的具體種類, 這其中涉及到了條件判斷, 因此我們需要超函式的幫助. 這個超函式所要做的就是判斷兩個給出的型別是否為同一個型別, 那麼它自然也就可以判斷疊代器對應的疊代器種類是否為我們需要的型別
不妨叫這個超函式為 is_same_type
, 那麼應該如何實作它呢? 我們應該先理解這個超函式具體要幹什麼. 假如給出 2 個型別, 如果是一樣的型別, 那麼超函式的回傳結果是 true
; 否則, 回傳型別是 false
. 在介紹超函式的時候, 我們介紹過超函式可以有兩種形式回傳超函式的回傳值, 一個是型別, 一個是具體的值. 為了方便起見, 我們先讓它回傳值 :
template <typename T, typename U>
struct is_same_type {
constexpr static auto value {false};
};
template <typename T>
struct is_same_type<T, T> {
constexpr static auto value {true};
};
當給定的兩個型別是一樣的, 那麼自動特製化到 is_same_type<T, T>
, 此時 value
的值為 true
; 此外的其它情況, value
的值全都是 false
. 現在, 我們再次嘗試實現上面這個需求 :
#include <iostream>
using namespace std;
template <typename T, typename U>
struct is_same_type {
constexpr static auto value {false};
};
template <typename T>
struct is_same_type<T, T> {
constexpr static auto value {true};
};
template <typename InputIterator>
void func(InputIterator begin, InputIterator end) {
if(is_same_type<typename iterator_traits<InputIterator>::iterator_category, random_access_iterator_tag>::value) {
clog << "iterator checking passed!" << endl;
}
}
int main(int argc, char *argv[]) {
func(static_cast<int *>(nullptr), static_cast<int *>(nullptr)); //輸出 : iterator checking passed!
func(0, 0); //Error : no type named 'iterator_category' in 'std::__1::iterator_traits<int>'
}
那麼, 我們已經基本解決了疊代器的問題. 此時, 我們增加一個多載函式, 使這個函式像 vector
一樣支援 vector(0, 0)
這樣的呼叫. 通過閱讀標準程式庫的原始碼, 我們發現, 類似於 func(0, 0)
這樣的呼叫, 匹配的函式原型是 :
func(size_type, const T &);
我們將 T 替換為 int, 於是有 :
#include <iostream>
using namespace std;
template <typename T, typename U>
struct is_same_type {
constexpr static auto value {false};
};
template <typename T>
struct is_same_type<T, T> {
constexpr static auto value {true};
};
template <typename InputIterator>
void func(InputIterator begin, InputIterator end) {
if(is_same_type<typename iterator_traits<InputIterator>::iterator_category, random_access_iterator_tag>::value) {
clog << "iterator checking passed!" << endl;
}
}
void func(unsigned long, int) {
clog << "unsigned long, long" << endl;
}
int main(int argc, char *argv[]) {
func(static_cast<int *>(nullptr), static_cast<int *>(nullptr)); //OK
func(0, 0); //Error : no type named 'iterator_category' in 'std::__1::iterator_traits<int>'
}
我們發現, 儘管多了一個 func
函式的多載, 但是仍然會產生和剛才一樣的編碼錯誤, 因為 func(0, 0)
優先匹配了 func(InputIterator InputIterator)
這個函式. 這是由於 0
預設是 int
型別, 對於具現化而來的 func(int, int)
和 func(unsigned long, int)
來說, 顯然 func(int, int)
更加匹配. 但是, 大家在使用標準程式庫的 vector
的時候, 肯定不會出現這樣的問題
這裡涉及到了我們從未講過的概念 : 替換. 在《C++ 2a 特性導讀 : Concept》中, 我們提到了影射, 這是類似的概念. 替換就是在樣板具現化的過程中, 將樣板的型別替換為實際的型別
我們隨意寫一個樣板程式 :
struct Foo {
using type = int;
};
template <typename T>
void func(typename T::type) {}
int main(int argc, char *argv[]) {
func(0); //Error : no matching function for call to 'func'. candidate template ignored: couldn't infer template argument 'T'
}
如果大家看得仔細的話, 大家會發現編碼錯誤和普通的函式無法匹配的編碼錯誤有些不一致, 我們再來看看普通的函式無法匹配, 是什麼樣的編碼錯誤 :
void func2(int) {}
int main(int argc, char *argv[]) {
func2(""); //no matching function for call to 'func2', candidate function not viable: no known conversion from 'const char [1]' to 'int' for 1st argument
}
普通的函式無法匹配是說型別無法轉換為函式參數中規定的型別. 而樣板函式無法匹配, 編碼器則是告訴我們預選的函式樣板被忽略. 也就是說, 當函式樣板的樣板參數被替換為實際參數的時候, 替換失敗了
這時, 我們有個疑問 : 替換失敗就一定失敗了嗎?
回答是 : SFINAE
來看一個例子 :
struct Foo {
using type = int;
};
struct Bar {
using type2 = int;
};
template <typename T>
void func(typename T::type) {}
template <typename T>
void func(typename T::type2) {}
int main(int argc, char *argv[]) {
func<Foo>(0); //OK
func<Bar>(0); //OK
}
當 func<Bar>(0)
嘗試匹配具現化的
template <>
void func<Bar>(typename Bar::type);
的時候, 發現 Bar
裡面沒有這樣的型別. 此時, 編碼器並沒有放棄繼續匹配其它函式, 因為確實多載集合內部還有別的函式可以去嘗試匹配, 直接擲出無法匹配的編碼錯誤顯然是不太負責任的. 當匹配到具現化的
template <>
void func<Bar>(typename Bar::type2) {}
的時候, 成功了. 接下來直接執行函式內部的程式碼就可以了
通過對上面程式碼和編碼錯誤的分析, 我們知道, 當編碼器替換一個函式樣板後, 嘗試匹配但是失敗了, 編碼器並不會直接放棄, 因此並不會直接擲出編碼錯誤. 只有所有的替換以及轉型都不生效, 此時還沒有匹配的函式的時候, 才會擲出編碼錯誤. 總結這句話, 也就有了 - SFINAE
SFINAE 是 "Substitution failure is not an error." 的縮寫, 取每個單詞的首字母然後全大寫即可. 翻譯成中文 : 替換失敗不是一個錯誤
下一篇文章, 我們將會詳細講述 SFINAE, 這篇文章我只是初步地介紹
這個時候, 我們應該還有一個疑問, 替換是什麼時候都能生效的嗎?
struct Foo {
using type = int;
};
template <typename T>
void func(typename T::type) {}
int main(int argc, char *argv[]) {
func(0); //no matching function for call to 'func'
}
上述程式碼將 func<Foo>(0)
改為了 func(0)
, 結果出現了無法函式匹配的編碼錯誤
而我們希望出現的情況是 : 編碼器自動找到 Foo
, 使用 Foo
替換樣板參數, 最終 typename T::type
被替換為 typename Foo::type
, 也就是 int
型別, 然後通過編碼執行函式內部的程式碼. 沒錯, 這樣是說得通的, 但是大家要想到, 不僅僅 Foo
裡面可能會有 type
型別, 萬一別的檔案中也有這樣一個類別, 類別內有一個 type
型別, 它是 int
的別名, 編碼器應該選擇哪個作為 T
的最終型別呢? 顯然編碼器無法選擇, 於是編碼器對於這種情況, 乾脆作出了 "如果你不明確 T
的型別, 我就不進行選擇" 這樣的決議. 最終, 有了如編碼器提示的編碼錯誤
所以如果遇到上述情況的程式碼, 大家一定要記得給出樣板引數
回到我們開頭所說的, 我們最終的目的是寫出類似於標準庫容器建構子這樣的多載函式, 使這些函式的行為和標準程式庫一樣. 結合上面所說, 我們只要製造出編碼器在替換的時候會替換失敗的型別就可以了. 目前, 大家可以得出, 對於以下這樣的程式碼 :
template <typename T>
void func(typename T::type) {}
int main(int argc, char *argv[]) {
func(0); //Error
}
編碼錯誤的原因是 int
是一個內建型別, int
內部不存在一個 type
的別名, 所以編碼器在替換的過程中替換失敗了. 我們參照這樣的程式碼, 寫出了如下的程式碼 :
#include <iostream>
using namespace std;
template <typename T, typename U>
struct is_same_type {
constexpr static auto value {false};
};
template <typename T>
struct is_same_type<T, T> {
constexpr static auto value {true};
};
template <typename T, typename>
struct out {
using type = T;
};
template <typename InputIterator>
void func(typename out<InputIterator, typename iterator_traits<InputIterator>::iterator_category>::type, InputIterator) {
if(is_same_type<typename iterator_traits<InputIterator>::iterator_category, random_access_iterator_tag>::value) {
clog << "iterator checking passed!" << endl;
}
}
void func(unsigned long, int) {
clog << "unsigned long and int" << endl;
}
int main(int argc, char *argv[]) {
func(static_cast<int *>(nullptr), static_cast<int *>(nullptr)); //輸出 : iterator checking passed!
func(0, 0); //輸出 : unsigned long and int
}
上面程式碼中, 我增加了一名為 out
的超函式. 其實一般來看, 這個 out
超函式相當簡單, 就是選擇第一個樣板參數 T
作為 type
的別名, 但是大家要注意到第二個樣板參數, 這個樣板參數是不具名的, 也就是 out
內部用不到它, 自然它的名稱就可以被省略. 超函式 out
真正的用意是它應該被用在函式的參數中, 當第二個樣板參數真實存在的時候, 回傳樣板參數 T
為這個超函式的結果. 結合 SFINAE, 我們自然想到, 如果一個型別不是疊代器, 那麼它內部幾乎不可能有 iterator_category
這個型別別名, 於是當萃取不到這個標籤的時候, 就會發生替換失敗. 而事實卻是是這樣的, 我們注意 func
函式的宣告 :
template <typename InputIterator>
void func(typename out<InputIterator, typename iterator_traits<InputIterator>::iterator_category>::type, InputIterator);
這個函式的第一個參數是 typename out<InputIterator, typename iterator_traits<InputIterator>::iterator_category>::type
, 這看起來有些複雜, 實際上非常簡單. out
的第一個樣板參數是 InputIterator
, 也就是我們希望如果第二個樣板參數有實際的引數來替換它, 那麼 InputIterator
就會成為回傳型別. 第二個樣板參數我們放入 typename iterator_traits<InputIterator>::iterator_category
, 這是對一個疊代器型別的萃取, 萃取它內部的 iterator_category
型別別名, 也就是疊代器標籤. 對於
func(static_cast<int *>(nullptr), static_cast<int *>(nullptr));
typename iterator_traits<int *>::iterator_category
就是 random_access_iterator_tag
的型別別名 (所有指標都是一個隨機訪問疊代器, 標準程式庫對 T *
型別的 iterator_traits
進行了特製化), 由於 random_access_iterator_tag
確實存在, 因此它被替換到了超函式 out
樣板參數的第二個位置. 第一個樣板函式的參數是 int *
, 最終它的回傳型別 type
就是 int *
型別的型別別名. 而對於
func(0, 0);
typename iterator_traits<int>::iterator_category
是不存在的型別, 因為 int
是一個內建型別, 不存在任何內部的型別別名, 因此一個不存在的型別不可能被替換到超函式 out
的樣板參數第二個位置. 於是發生了替換失敗, 這個函式的匹配就被忽略了. 由於編碼器這時候還會尋找其它可能的匹配, 而多載的函式集合中卻是還有一個
func(unsigned long, int);
可以拿來匹配, 所以當將第一個函式引數 int
轉型到 unsigned long
之後, 就完成了匹配. 故最終匹配到了上面這個函式
如果上述的解釋你還沒有看懂, 我可以用另外一種方式和你解釋一下. 我們在使用標準程式庫的 vector
的時候, 有這兩種用法 :
#include <iostream>
#include <vector>
using namespace std;
int main(int argc, char *argv[]) {
vector<int> vec1(10, 10); //這個 vector 內存有 10 個 10
int arr[] {1, 2, 3, 4, 5, 6, 7, 8};
vector<int> vec2(begin(arr), end(arr)); //這個 vector 內存有 1, 2, 3, 4, 5, 6, 7, 8 這 8 個數字
}
這兩個 vector
使用的建構子分別是 :
template <typename T>
class vector {
//...
public:
vector(size_t, const T &);
vector(T *, T *);
//...
};
但是疊代器不一定是指標型別, 它可能是自訂的類別, 因此必須使用泛型. 於是, 上述程式碼就被修改為下面的程式碼 :
template <typename T>
class vector {
//...
public:
vector(size_t, const T &);
template <typename Iterator>
vector(Iterator, Iterator);
//...
};
現在將型別 T
替換為 int
, 那麼就有了這個具現體 :
template <>
class vector<int> {
//...
public:
vector(size_t, const int &);
template <typename Iterator>
vector(Iterator, Iterator);
//...
};
對於 vector<int> vec2(begin(arr), end(arr));
這樣的初始化顯然不會產生歧異. 但是對於 vector<int> vec1(10, 10);
這樣的初始化就會產生歧異. 因為它可以匹配上述兩個函式, 而樣板特製化出來的函式將會更加匹配, 這並不符合我們的原意
對於程式庫使用者來說, 我們希望在不改變上述使用習慣的情況下, 使用超編程使函式的匹配過程中匹配到我們想要的結果, 也就是函式的參數將會不變
此時我們想到, 如果有一個超函式, 首先放入疊代器的型別, 然後放入另外一個型別, 如果如果這個另外的型別沒有替換失敗, 那麼就輸出疊代器的型別, 最終函式的參數就可以保持不變了. 現在回到對超函式 out
的解釋, 你是否可以看懂了呢?
文章最後, 我希望給大家留意個練習. 因為現在是採用 if
陳述式來判斷疊代器的具體型別是否為隨機訪問疊代器, 如果在編碼器不優化的情況下, 它是有運作期消耗的. 現在我希望使用剛才所講述的全部知識, 將這個檢測移到編碼期來, 你能實作這個需求嗎?
自創文章, 原著 : Jonny, 如若需要轉發, 在已經授權的情況下請註明出處 :《【C++ Template Meta-Programming】函式多載與 SFINAE 初步》https://jonny.vip/2019/08/27/%e3%80%90cplusplus-template-meta-programming%e3%80%91%e5%87%bd%e5%bc%8f%e5%a4%9a%e8%bc%89%e8%88%87-sfinae-%e5%88%9d%e6%ad%a5/
Leave a Reply