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 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
// 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();
}
引子
构造函数是一个perfect forwarding,所以:
但是当尝试调用copy constructor时会报错:
但是如果参数是const Person或者move constructor则正确:
原因是:根据c++的重载规则,对于一个
nonconstant lvalue Person p
,member template会优于copy constructor
因为STR会直接被substituted为Person&,而copy constructor还需要一次const转换。
也许提供一个nonconstant copy constructor会解决这个问题,但是我们真正想做的是当参数是Person类型时,禁用掉member template。这可以通过
std::enable_if<>
来实现。使用enable_if<>禁用模板
当
sizeof(T) > 4
为False时,该模板就会被忽略。如果sizeof(T) > 4
为true时,那么该模板会被扩展为:std::enable_if<>是一种类型萃取(type trait),会根据给定的一个编译时期的表达式(第一个参数)来确定其行为:
std::enable_if<>::type
会返回:std::enable_if<>::type
不会被定义。根据下面会介绍的SFINAE(substitute failure is not an error), 这会导致包含std::enable_if<>的模板被忽略掉。给std::enable_if<>传递第二个参数的例子:
如果表达式为真,那么模板会被扩展为:
如果你觉得将enable_if<>放在声明中有点丑陋的话,通常的做法是:
当
sizeof(T) > 4
时,这会被扩展为:还有种比较常见的做法是配合using:
enable_if<>实例
我们使用enable_if<>来解决引子中的问题:
核心点:
所以下面的调用会按照预期方式执行:
注意在不同版本中的写法:
using EnableIfString = std::enable_if_t<std::is_convertible_v<T, std::string>>
using EnableIfString = std::enable_if_t<std::is_convertible<T, std::string>::value>
using EnableIfString = typename std::enable_if<std::is_convertible<T, std::string>::value>::type
使用Concepts简化enable_if<>
如果你还是觉得enable_if<>不够直观,那么可以使用之前文章提到过的C++20引入的Concept.
我们也可以将条件定义为通用的Concept:
甚至可以改为:
SFINAE (Substitution Failure Is Not An Error)
在C++中针对不同参数类型做函数重载时很常见的。编译器需要为一个调用选择一个最适合的函数。
当这些重载函数包含模板函数时,编译器一般会执行如下步骤:
但是替换的结果可能是毫无意义的。这时,编译器不会报错,反而会忽略这个函数模板。
我们将这个原则叫做:SFINAE(“substitution failure is not an error)
但是替换(substitute)和实例化(instantiation)不一样:即使最终不需要被实例化的模板也要进行替换(不然就无法执行上面的第3步)。不过它只会替换直接出现在函数声明中的相关内容(不包含函数体)。
考虑下面的例子:
当传递一个数组或者字符串时,只有第一个函数模板匹配,因为
T::size_type
导致第二个模板函数会被忽略:同理,传递一个vector会只有第二个函数模板匹配:
注意,这与传递一个对象,有size_type成员,但是没有size()成员函数不同。例如:
编译器会根据SFINAE原则匹配到第二个函数,但是编译器会报找不到
std::allocator<int>
的size()成员函数。在匹配过程中不会忽略第二个函数,而是在实例化的过程中报错。而使用enable_if<>就是实现SFINAE最直接的方式。
SFINAE with decltype
有的时候想要为模板定义一个合适的表达式是比较难得。
比如上面的例子,假如参数有size_type成员但是没有size成员函数,那么就忽略该模板。之前的定义为:
这么定义会导致编译器选择该函数但是会在instantiation阶段报错。
处理这种情况一般会这么做:
trailing return type
来指定返回类型 (auto -> decltype)比如:
这里,decltype的参数是一个逗号表达式,所以最后的
T::size_type()
为函数的返回值类型。逗号前面的(void)(t.size())
必须成立才可以。(完)
朋友们可以关注下我的公众号,获得最及时的更新: