1. 程式人生 > >c++11-17 模板核心知識(八)—— enable_if<>與SFINAE

c++11-17 模板核心知識(八)—— enable_if<>與SFINAE

- [引子](#引子) - [使用enable_if<>禁用模板](#使用enable_if禁用模板) - [enable_if<>例項](#enable_if例項) - [使用Concepts簡化enable_if<>](#使用concepts簡化enable_if) - [SFINAE (Substitution Failure Is Not An Error)](#sfinae-substitution-failure-is-not-an-error) - [SFINAE with decltype](#sfinae-with-decltype) ## 引子 ```c++ class Person { private: std::string name; public: // generic constructor for passed initial name: template explicit Person(STR &&n) : name(std::forward(n)) { std::cout << "TMPL-CONSTR for '" << name << "'\n"; } // copy and move constructor: Person(Person const &p) : name(p.name) { std::cout << "COPY-CONSTR Person '" << name << "'\n"; } Person(Person &&p) : name(std::move(p.name)) { std::cout << "MOVE-CONSTR Person '" << name << "'\n"; } }; ``` 建構函式是一個perfect forwarding,所以: ```c++ std::string s = "sname"; Person p1(s); // init with string object =>
calls TMPL-CONSTR Person p2("tmp"); // init with string literal => calls TMPL-CONSTR ``` 但是當嘗試呼叫copy constructor時會報錯: ```c++ Person p3(p1); // ERROR ``` 但是如果引數是const Person或者move constructor則正確: ```c++ Person const p2c("ctmp"); // init constant object with string literal Person p3c(p2c); // OK: copy constant Person =>
calls COPY-CONSTR Person p4(std::move(p1)); // OK: move Person => calls MOVE-CONST ``` **原因是:根據c++的過載規則,對於一個`nonconstant lvalue Person p`,member template** ```c++ template Person(STR&& n) ``` **會優於copy constructor** ```c++ Person (Person const& p) ``` 因為STR會直接被substituted為Person&,而copy constructor還需要一次const轉換。 也許提供一個nonconstant copy constructor會解決這個問題,但是我們真正想做的是當引數是Person型別時,禁用掉member template。這可以通過`std::enable_if<>`來實現。 ## 使用enable_if<>禁用模板 ```c++ template typename std::enable_if<(sizeof(T) > 4)>::type foo() { } ``` 當`sizeof(T) > 4`為False時,該模板就會被忽略。如果`sizeof(T) > 4`為true時,那麼該模板會被擴充套件為: ```c++ void foo() { } ``` std::enable_if<>是一種型別萃取(type trait),會根據給定的一個編譯時期的表示式(第一個引數)來確定其行為: * 如果這個表示式為true,`std::enable_if<>::type`會返回: * 如果沒有第二個模板引數,返回型別是void。 * 否則,返回型別是其第二個引數的型別。 * 如果表示式結果false,`std::enable_if<>::type`不會被定義。根據下面會介紹的SFINAE(substitute failure is not an error), 這會導致包含std::enable_if<>的模板被忽略掉。 給std::enable_if<>傳遞第二個引數的例子: ```c++ template std::enable_if_t<(sizeof(T) > 4), T> foo() { return T(); } ``` 如果表示式為真,那麼模板會被擴充套件為: ```c++ MyType foo(); ``` 如果你覺得將enable_if<>放在宣告中有點醜陋的話,通常的做法是: ```c++ template 4)>> void foo() { } ``` 當`sizeof(T) > 4`時,這會被擴充套件為: ```c++ template void foo() { } ``` 還有種比較常見的做法是配合using: ```c++ template using EnableIfSizeGreater4 = std::enable_if_t<(sizeof(T) > 4)>; template> void foo() { } ``` ## enable_if<>例項 我們使用enable_if<>來解決引子中的問題: ```c++ template using EnableIfString = std::enable_if_t>; class Person { private: std::string name; public: // generic constructor for passed initial name: template > explicit Person(STR &&n) : name(std::forward(n)) { std::cout << "TMPL-CONSTR for '" << name << "'\n"; } // copy and move constructor: Person(Person const &p) : name(p.name) { std::cout << "COPY-CONSTR Person '" << name << "'\n"; } Person(Person &&p) : name(std::move(p.name)) { std::cout << "MOVE-CONSTR Person '" << name << "'\n"; } }; ``` 核心點: * 使用using來簡化std::enable_if<>在成員模板函式中的寫法。 * 當建構函式的引數不能轉換為string時,禁用該函式。 所以下面的呼叫會按照預期方式執行: ```c++ int main() { std::string s = "sname"; Person p1(s); // init with string object => calls TMPL-CONSTR Person p2("tmp"); // init with string literal => calls TMPL-CONSTR Person p3(p1); // OK => calls COPY-CONSTR Person p4(std::move(p1)); // OK => calls MOVE-CONST } ``` 注意在不同版本中的寫法: * C++17 : `using EnableIfString = std::enable_if_t>` * C++14 : `using EnableIfString = std::enable_if_t::value>` * C++11 : `using EnableIfString = typename std::enable_if::value>::type` ## 使用Concepts簡化enable_if<> 如果你還是覺得enable_if<>不夠直觀,那麼可以使用之前文章提到過的C++20引入的Concept. ```c++ template requires std::is_convertible_v Person(STR&& n) : name(std::forward(n)) { ... } ``` 我們也可以將條件定義為通用的Concept: ```c++ template concept ConvertibleToString = std::is_convertible_v; ... template requires ConvertibleToString Person(STR&& n) : name(std::forward(n)) { ... } ``` 甚至可以改為: ```c++ template Person(STR&& n) : name(std::forward(n)) { ... } ``` ## SFINAE (Substitution Failure Is Not An Error) 在C++中針對不同引數型別做函式過載時很常見的。編譯器需要為一個呼叫選擇一個最適合的函式。 當這些過載函式包含模板函式時,編譯器一般會執行如下步驟: * 確定模板引數型別。 * 將函式引數列表和返回值的模板引數替換掉(substitute) * 根據規則決定哪一個函式最匹配。 但是替換的結果可能是毫無意義的。這時,編譯器不會報錯,反而會忽略這個函式模板。 我們將這個原則叫做:SFINAE(“substitution failure is not an error) 但是替換(substitute)和例項化(instantiation)不一樣:即使最終不需要被例項化的模板也要進行替換(不然就無法執行上面的第3步)。不過它只會替換直接出現在函式宣告中的相關內容(不包含函式體)。 考慮下面的例子: ```c++ // number of elements in a raw array: template std::size_t len(T (&)[N]) { return N; } // number of elements for a type having size_type: template typename T::size_type len(T const &t) { return t.size(); } ``` 當傳遞一個數組或者字串時,只有第一個函式模板匹配,因為`T::size_type`導致第二個模板函式會被忽略: ```c++ int a[10]; std::cout << len(a); // OK: only len() for array matches std::cout << len("tmp"); // OK: only len() for array matches ``` 同理,傳遞一個vector會只有第二個函式模板匹配: ```c++ std::vector v; std::cout << len(v); // OK: only len() for a type with size_type matches ``` 注意,這與傳遞一個物件,有size_type成員,但是沒有size()成員函式不同。例如: ```c++ std::allocator x; std::cout << len(x); // ERROR: len() function found, but can’t size() ``` 編譯器會根據SFINAE原則匹配到第二個函式,但是編譯器會報找不到`std::allocator`的size()成員函式。在匹配過程中不會忽略第二個函式,而是在例項化的過程中報錯。 **而使用enable_if<>就是實現SFINAE最直接的方式。** ### SFINAE with decltype 有的時候想要為模板定義一個合適的表示式是比較難得。 比如上面的例子,假如引數有size_type成員但是沒有size成員函式,那麼就忽略該模板。之前的定義為: ```c++ template typename T::size_type len (T const& t) { return t.size(); } std::allocator x; std::cout << len(x) << '\n'; // ERROR: len() selected, but x has no size() ``` 這麼定義會導致編譯器選擇該函式但是會在instantiation階段報錯。 處理這種情況一般會這麼做: * 通過`trailing return type`來指定返回型別 (auto ->
decltype) * 將所有需要成立的表示式放在逗號運算子的前面。 * 在逗號運算子的最後定義一個型別為返回型別的物件。 比如: ```c++ template auto len (T const& t) -> decltype( (void)(t.size()), T::size_type() ) { return t.size(); } ``` 這裡,decltype的引數是一個逗號表示式,所以最後的`T::size_type()`為函式的返回值型別。逗號前面的`(void)(t.size())`必須成立才可以。 (完) 朋友們可以關注下我的公眾號,獲得最及時的更新: ![](https://img2020.cnblogs.com/blog/1298604/202011/1298604-20201103132150036-885052