c++11 std::enable_if在模板偏特化的妙用

1.模板自动推导功能。

先看个例子:

在调用TestTemplate函数时, 我们可以在函数后面加上<类型>无歧义地指定调用的版本。

 结果如下:

 由于模板参数在函数参数中的位置是固定的,编译器其实可以推导出参数的类型, 这样程序员们就可以不指定模板的类型来调用,代码更加简洁清晰通用,不会出现写错模板类型的错误,如下:

 模板自动推导是如此的美好,我们要好好地利用它。 在应用过程中, 也引入了一些问题, 有些情况, 编译器发现某些代码满足多个同名函数模板,无法决定特化成哪一个,便会报错; 或者编译器不报错了,但是我们自己都不确定编译器特化成哪一个, 存在巨大的隐患。

因此,十分有必要搞清楚特化的规则。

2.模板函数推导的优先级。

a.不带模板的函数最优先。

例如增加专门针对int类型的普通函数实现:

结果如下:

 

 当参数是int时, 调用了普通函数的版本, 虽然模板也满足,但是普通函数优先。

如果确实需要调用模板的版本,可以通过指定模板参数类型来实现。

如下:

 这样就能调用到模板的版本。

b.模板参数个数越少的越优先(不建议参考)。

我在VS2017测试如下:

 

TestTemplate接受2个类型T1, T2。以上输出就是调用模板实现。

此处没有歧义,谁赞同,谁反对? 

梁家辉谁赞成谁反对表情包_动态图字幕在线制作_GIF之家

这时增加一个模板函数, 对于T1和T2是同类型时,作出处理。

 此时,编译器会调用哪个?是否明确?

我在VS2017上测试,发现调用了模板参数少的,结果如下: 

在标准上不知道有没有规定这种情况应该实例化哪个函数, 为了避免引起误会, 建议不能依赖它。 写代码时要避开有歧义的坑, 确保读者写者都对含义的理解完全一致。 后面std::enable_if完全可以清晰地表达我们想要编译器实例化成哪个函数。

3.模板类型推导冲突。

当一个模板函数推导,没有特化满足,或者多于一个特化满足时,编译器都会报错,罢工。

假设我要实现一个转换函数,让不同的类型相互转换,例如short转成int, int 转成string等。

其中从std::string转换成基本类型,或者基本类型转换成std::string, 我希望引入第三方的串行化库来实现,这时,分别对这两种情况进行了模板特化。

 以上代码运行良好,输出如下:

当我们增加一行从std::string转换到std::string的模板调用时, 编译器就不干了。

 因为这个调用, 以上3个模板定义都满足, 而且无法通过指定模板类型的方法来解决。Transform<std::string>指定了模板参数类型后,依然是冲突的。

解决方案一:

 增加一个std::string转std::string的普通函数重载,根据普通函数优先的规则, 编译器锁定了调用的版本,不再有歧义。缺点就是代码对于std::string转std::string的逻辑表达上是冗余的,同一份逻辑,在两个地方重复定义了,而重复是万恶之源。 对于在c++11以前, 也没有更好的表达办法了。

解决方案二:

在语法上明确地告知编译器,我们想要它怎样去选择。在c++11以后,我们有了这样的表达工具,那就是std::enable_if。具体用法在后面介绍。

4.std::enable_if用法

enable_if 的主要作用就是:当某个条件成立时,enable_if表示指定的类型;当条件不成立时, enable_if表示未定义,但不会报错,编译器在实例化时就会忽略它,最终定位在唯一符合实例化的实现。声明如下:

template<bool Cond, class T = void> struct enable_if;

a.SFINAE特性

SFINAE 全称是 Substitution Failure Is Not An Error。 当模板定义中出现了一些符合c++语法,但是不存在的函数,或者变量等。只要没有真正实例化, 也不会报错。

例如:

T::testError函数,在没有实例化之前, 编译器都不会去考虑它是否存在,是否合法。只要不违反c++语法就可以了。

违反C++语法的,即使没有实例化,也会报错的。例如输入了一些中文:

b.有了SFINAE特性, std::enable_if才能放心地大展拳脚。

现在我们来解决前面提到的std::string转换成std::string的特化问题。

先写出了Transform函数的通用表达如下:

之前的所有Transform调用都是通过的。

typename std::enable_if<true, "类型">::type   等价于   "类型"

typename std::enable_if<false, "类型">::type  等价于  未定义

<==>表示等价于, 那么:

typename std::enable_if<true, bool>::tpye <==> bool

typename std::enable_if<true, int>::tpye <==> int

typename std::enable_if<true, T1>::tpye <==> T1

typename std::enable_if<false, bool>::tpye <==> 未定义

typename std::enable_if<false, int>::tpye <==> 未定义

typename std::enable_if<false, T1>::tpye <==> 未定义

未定义表示, 该类型标识所在的函数等价于没有定义, 编译器在实例化时会忽略它。

Transform函数修改如下:

 由于std::enable_if中的condition固定为true,  

typename std::enable_if<true, bool>::tpye <==> bool

整个模板函数,跟修改前其实时一样的,因此编译也通过。

一旦把std::enable_if里的条件改成false, 编译器便报错。 

std::is_same用于判断两个类型是否相同。

std::is_same(T1, T2)::value的结果就是bool类型。

 如果T1,T2类型相同,std::is_same(T1, T2)::value = true;

 如果T1,T2类型不同,std::is_same(T1, T2)::value = false;

std::is_same等函数与std::enable_if组合起来就可以实现根据类型选择不同的模板特化的精准控制。

例如在以上基础上增加std::is_same(T1, T2)判断:

编译器提示T1与T2类型不相同的模板实例化失败。 而T1和T2类型相同情况,就精准地调用了我们对类型相同判断后的特化。

接下来我们分别对

目标是字符串,但源不是。

以及目标不是字符串, 但源是的情况做特化。

结果准确地如我们所愿:

并且当我们把int转成unsigned short时, 它能准确地报错,告知我们该转换需要另外实现。

 

std::enable_if用法规则如上, 除了可以用在函数返回值上, 还能
a.用在函数末尾增加一个冗余的参数。

例如:

用在函数末尾,表示函数的最后一个参数的类型根据T1, T2的判断来决定, 判断为true,  则正确定义了一个冗余的类型指针,成功模板实例化, 并且由于指定了默认参数,调用者与原来的调用一致。 类型判断结果为false时, 类型未定义,实例化时自动忽略跳过。 比std::enable_if用在函数返回值更清晰一些。

b.用在模板的参数上。

模板参数支持bool, int, 指针等类型,把std::enable_if用在模板的参数上, 函数的形式与原来完全一致,  我更加偏向于使用这种方式。

最后提醒一点, 前面所有例子对于类型的判别都没有做去除修饰词操作。

例如模板中的T1, T2, 有时候并非推导成某个类型type, 可能是type&,  可能是const type, 也可能是const type&, 为了准确地使用std::is_same等函数判断类型, 通常还需要对T1, T2做一个移除修饰词的操作。 std::decay就是专门做这个事情。

无论T1推导成int, 或者int&,或者const int, 或者const int&, std::decay<T1>::type总是表示int。

因此最终实际使用时, 所有模板标签判断语句都应该加上std::decay。

最终变成:

std::is_same<typename std::decay<T1>::type, typename std::decay<T2>::type>::value,

或者std::is_same<std::decay_t<T1>, std::decay_t<T2>>::value。

c++11 type_traits 增加了很多对于类型判断的功能,例如std::is_integral std::is_class, std::is_enum等...

详情参考标准https://en.cppreference.com/w/cpp/header/type_traits

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值