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