c++11-17 模板核心知识(八)—— enable_if<>与SFINAE
- 引子
- 使用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())必须成立才可以。
(完)
朋友们可以关注下我的公众号,获得最及时的更新:

c++11-17 模板核心知识(八)—— enable_if<>与SFINAE的更多相关文章
- c++11-17 模板核心知识(十一)—— 编写泛型库需要的基本技术
Callables 函数对象 Function Objects 处理成员函数及额外的参数 std::invoke<>() 统一包装 泛型库的其他基本技术 Type Traits std:: ...
- c++11-17 模板核心知识(十二)—— 模板的模板参数 Template Template Parameters
概念 举例 模板的模板参数的参数匹配 Template Template Argument Matching 解决办法一 解决办法二 概念 一个模板的参数是模板类型. 举例 在c++11-17 模板核 ...
- c++11-17 模板核心知识(十五)—— 解析模板之依赖型类型名称与typename Dependent Names of Types
模板名称的问题及解决 typename规则 C++20 typename 上篇文章c++11-17 模板核心知识(十四)-- 解析模板之依赖型模板名称 Dependent Names of Templ ...
- c++11-17 模板核心知识(十四)—— 解析模板之依赖型模板名称(.template/->template/::template)
tokenization与parsing 解析模板之类型的依赖名称 Dependent Names of Templates Example One Example Two Example Three ...
- c++11-17 模板核心知识(二)—— 类模板
类模板声明.实现与使用 Class Instantiation 使用类模板的部分成员函数 Concept 友元 方式一 方式二 类模板的全特化 类模板的偏特化 多模板参数的偏特化 默认模板参数 Typ ...
- c++11-17 模板核心知识(三)—— 非类型模板参数 Nontype Template Parameters
类模板的非类型模板参数 函数模板的非类型模板参数 限制 使用auto推断非类型模板参数 模板参数不一定非得是类型,它们还可以是普通的数值.我们仍然使用前面文章的Stack的例子. 类模板的非类型模板参 ...
- c++11-17 模板核心知识(一)—— 函数模板
1.1 定义函数模板 1.2 使用函数模板 1.3 两阶段翻译 Two-Phase Translation 1.3.1 模板的编译和链接问题 1.4 多模板参数 1.4.1 引入额外模板参数作为返回值 ...
- c++11-17 模板核心知识(五)—— 理解模板参数推导规则
Case 1 : ParamType是一个指针或者引用,但不是universal reference T& const T& T* Case 2 : ParamType是Univers ...
- c++11-17 模板核心知识(九)—— 理解decltype与decltype(auto)
decltype介绍 为什么需要decltype decltype(auto) 注意(entity) 与模板参数推导和auto推导一样,decltype的结果大多数情况下是正常的,但是也有少部分情况是 ...
随机推荐
- 第二章 rsync服务原理
一.备份 1.什么是备份? 1)把重要的数据或者文件再次复制一份并保存下来 2.为什么要做备份? 1)数据的重要性 2)为了出现故障,恢复数据 3.能不能不备份? 1)重要的数据一定要备份 2)不重要 ...
- 【API进阶之路】研发需求突增3倍,测试团队集体闹离职
摘要:最近研发的需求量涨了3倍,开发团队拼命赶进度,可苦了测试团队. 本以为从一线研发转管理后会清闲一些,但是没想到,我还要充当救火队员的角色. 到了第四季度,各业务部门都在憋着劲儿冲业绩,毕竟这跟年 ...
- 类型转化 - js中的骚操作
Number Number() 把字符串数字转化成数字类型,布尔类型也可以转化 parseInt parseInt() 字符串数字转化成数字类型,当布尔类型不可以(NaN),但该函数可以把数字开头的数 ...
- UbuntuStudio20.04安装教程(双系统安装,windows10已安装)
硬件和系统: acer4750(原i3换i7,加固态硬盘200多G,原机械硬盘500G由光驱改装,内存由2G增加为6G)2010年购买3300,性价比高,硬件升级后2020年不过时 windows10 ...
- 计算机二级考试:Java
目录 第 1 章 Java 语言概论 第 2 章 基本数据类型 2.1 概述 2.1.1 标识符 2.1.2 关键字 2.1.3 常量 2.2 基本数据类型 第 3 章 运算符和表达式 3.2 算术运 ...
- 关于Android Studio中使用jni进行opencv配置开发环境的要素秘诀
使用jni进行opencv开发可以快速地将PC端的opencv代码移植到手机上,但是如何在android studio下进行配置,网上几乎找不到教程,大多都是eclipse下使用mk文件的方法,找不到 ...
- 微信小程序-TodoList
TodoList 博客班级 https://edu.cnblogs.com/campus/zjcsxy/SE2020 作业要求 https://edu.cnblogs.com/campus/zjcsx ...
- MapStruct 解了对象映射的毒
前言 MVC模式是目前主流项目的标准开发模式,这种模式下框架的分层结构清晰,主要分为Controller,Service,Dao.分层的结构下,各层之间的数据传输要求就会存在差异,我们不能用一个对象来 ...
- 【SpringBoot】16. 如何监控springboot的健康状况
如何监控springboot的健康状况 SpringBoot1.5.19.RELEASE 一.使用Actuator检查与监控 actuaotr是spring boot项目中非常强大的一个功能,有助于对 ...
- ubuntu16安装ROS(包括win10子系统ubuntu同样能用)
1. sudo sh -c 'echo "deb http://packages.ros.org/ros/ubuntu $(lsb_release -sc) main" > ...