c++11-17 模板核心知識(八)—— enable_if<>與SFINAE
阿新 • • 發佈:2020-11-23
- [引子](#引子)
- [使用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