You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
classPerson {
private:
std::string name;
public:// generic constructor for passed initial name:template <typename STR>
explicitPerson(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 constp2c("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>>;
classPerson {
private:
std::string name;
public:// generic constructor for passed initial name:template <typename STR, typename = EnableIfString<STR>>
explicitPerson(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时,禁用该函数。
所以下面的调用会按照预期方式执行:
intmain() {
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_tlen(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<>传递第二个参数的例子:
如果表达式为真,那么模板会被扩展为:
MyType foo();
如果你觉得将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())
必须成立才可以。(完)
朋友们可以关注下我的公众号,获得最及时的更新:
The text was updated successfully, but these errors were encountered: