跟我学c++高级篇——模板元编程之六SFINAE

一、SFINAE

SFINAE,Substitution failure is not an error,翻译成中文可理解为“匹配失败并不是一个错误”。也即“在函数模板的重载决议中会应用此规则:当模板形参在替换成显式指定的类型或推导出的类型失败时,从重载集中丢弃这个特化,而非导致编译失败。”
如果在几年前,还是要认真的分析一下SFINAE的,不过随着c++20中concepts的推出,SNIFAE也就注定会慢慢隐入尘烟。但是在元编程中,代码中经常又遇到,所以还要分析一下。
个人的建议是,知其然即可。当然,深入进去,肯定没有坏处,毕竟c++的发展是延袭而来的。

二、分析

SFINAE主要的作用就如它的名字一样,在编译期模板展开时或者在有模板泛型参与时的匹配列表时,出现一些问题时,不会千万编译过程的中断。同时,在元编程,可以用来实现编译期的和反射(如果对这两个概念不明白,可以翻看前面的“跟我学c++中级篇——再谈Concepts”)。
所以说一种技术的出现和发展,不是从后来眼光来看它的优秀与否,而是要结合当时的环境来分析。在c++这门语言无法实现类似JAVA等高级语言一些高级功能并且尚未有新的标准出现前,SFINAE可以看作一些技术大佬们的有益的探索和对c++一些缺陷的自我完善。
在实际的应用中,最典型的就是判断模板替换的类型是否是class(或者普通类型)或者其是否拥有指定的成员函数或者成员变量。在前面分析说明一Concepts和一些模板编程中的应用中,泛型导致的后果是与c++强类型的判定极可能出现冲突,比如开放接口中对一些变量的限定和成员函数的必须实现。如果没有约束,那么极有可能在编译时出现问题。
同样,做为对特定类型的处理,比如false或者true类型时,或者具体到某一类型偏特化,如何进行特定的实例化,函数模板也是如此。
下面看一个例子:

template<typename A>
struct B { using type = typename A::type; };

template<
    class T,
    class U = typename T::type,    // 如果 T 没有成员 type 那么就是 SFINAE 失败
    class V = typename B<T>::type> // 如果 T 没有成员 type 那么就是硬错误
                                   // (经由 CWG 1227 保证不出现,
                                   // 因为到 U 的默认模板实参中的替换会首先失败)
void foo (int);

template<class T>
typename T::type h(typename B<T>::type);

template<class T>
auto h(typename B<T>::type) -> typename T::type; // 重声明

template<class T>
void h(...) {}

using R = decltype(h<int>(0));     // 错误格式,不要求诊断

再看一个类型处理的:

template <int I>
struct X {};

template<template<class T> class>
struct Z {};

template<class T>
void f(typename T::Y*) {}

template<class T>
void g(X<T::N>*) {}

template<class T>
void h(Z<T::template TT>*) {}

struct A {};
struct B { int Y; };
struct C { typedef int N; };
struct D { typedef int TT; };
struct B1 { typedef int Y; };
struct C1 { static const int N = 0; };
struct D1
{
    template<typename T>
    struct TT {};
};

int main()
{
    // 下列各个情况推导失败:
    f<A>(0); // 不含成员 Y
    f<B>(0); // B 的 Y 成员不是类型
    g<C>(0); // C 的 N 成员不是非类型
    h<D>(0); // D 的 TT 成员不是模板

    // 下列各个情况推导成功:
    f<B1>(0);
    g<C1>(0);
    h<D1>(0);
}
// 未完成:需要演示重载决议,而不只是失败

偏特化的处理:

// 主模板处理无法引用的类型:
template<class T, class = void>
struct reference_traits
{
    using add_lref = T;
    using add_rref = T;
};

// 特化识别可以引用的类型:
template<class T>
struct reference_traits<T, std::void_t<T&>>
{
    using add_lref = T&;
    using add_rref = T&&;
};

template<class T>
using add_lvalue_reference_t = typename reference_traits<T>::add_lref;

template<class T>
using add_rvalue_reference_t = typename reference_traits<T>::add_rref;

通过上面找例子,可以发现几个问题:
1、Substitution(形参被实参替代)什么时候产生
2、什么样的行为称为Substitution Failure,即SFINAE;反之什么是SFINAE error.
可以这样来说明:
1、对函数模板形参进行两次替换(由模板实参所替代):
显式指定的模板实参在模板实参推导前替换;推导出的实参和从默认项获得的实参在模板实参推导后替换。
替换发生于:
函数类型中使用的所有类型(包括返回类型和所有形参的类型)
各个模板形参声明中使用的所有类型
函数类型中使用的所有表达式
各个模板形参声明中使用的所有表达式(C++11 起)
explicit 说明符中使用的所有表达式(C++20 起)
2、以上类型或表达式在以用来替换的实参应用了错误格式的表达式(并带有必要的诊断)的场合是替换失败。
只有在函数类型或其模板形参类型或其 explicit 说明符 (C++20 起)的立即语境中的类型与表达式中的失败是 SFINAE error。如果对替换后的类型/表达式的求值导致副作用,例如实例化某模板特化、生成某隐式定义的成员函数等,那么这些副作用中的错误被当做硬错误。lambda 表达式不被当作是立即语境的一部分。 (C++20 起)

也就是说,要明白SFINAE,SFINAE error,hard error这三者不同:
比如像c++11标准前类型 使用常量表达式才会当成SFINAE,而在部分特化中出现匹配失败,编译器把其当成SFINAE,包括函数也是一样;另外对于词法顺序导致的失败也算错误格式也是SFINAE。还比如上面如果具体没有某个类型那么就会当成SFINAE,这些都是一些比较清楚的表述了。
而SFINAE error一般是指在实例化含有多个不同长度的Pack(c++11)或者创建非正常数组(0长度,void数组等等),以及在表达式中模板形参类型中使用的表达式格式错误,函数类型中使用了错误的表达式。
上面的例程中给出的一个hard error就是嵌套调用类型没有,则是一个硬错误。

三、常见应用

在SFINAE的应用中,诸如std::enable_if之类的应用很广,其它常见的还有std::same_as,std::is_integral,std::is_array,std::is_class,std::is_function,std::is_pointer,std::is_const等等。看一个原有的例程“C++20的新特性”就更清楚了:

template<typename T>
    class Ex {
    public:
      template<typename TS = T>
      typename std::enable_if_t<std::is_convertible<TS, std::string>::value, std::string>
      mystring() const {
        return "is owner";
      }
    };

    Ex<size_t> ex;                             
    ex.mystring(); //  ERROR!

    Ex<std::string> e;                        
    e.mystring();

还有“c++11完美转发”中的例程:


#include <string>
struct Data
{
	int d = 0;
};
//下面的代码开启默认值,则不再使用默认构造函数,引起错误
class ForwardSampleT
{
public:
	//普通构造函数---注意注释的默认值
	template <typename T, typename N = std::string>
	ForwardSampleT(T&& dt, N&& name="w")
		:dt_(std::forward<T>(dt)), name_(std::forward<N>(name))
	{
		std::cout << "call call call" << std::endl;
	};

private:
	Data dt_;
	std::string name_ = "";
};
//下面代码通过SFINAE来处理上面的异常情况,实际运行时,需要注释任何一个后再测试
class ForwardSampleT
{
public:
	//普通构造函数
	template <typename T, typename N = std::string,
		typename = std::enable_if_t<std::is_convertible_v<T, Data>>>
		ForwardSampleT(T&& dt, N&& name = "w")
		:dt_(std::forward<T>(dt)), name_(std::forward<N>(name))
	{
		std::cout << "call call call" << std::endl;
	};

private:
	Data dt_;
	std::string name_ = "";
};
void Test()
{
	Data da;
	ForwardSampleT fs = { da,"test" };//ForwardSampleT fs = { 0,"test" };
	ForwardSampleT fs1 = { fs };
}

这两个例子有几乎相同的功能,大家认真分析就明白了SFINAE技术的味道。换句话说,可以使用std::enable_if来禁用一些模板函数,比如上面的例子,但是不可以禁用c++类中默认的几个函数,如拷贝构造函数、移动构造函数,赋值构造函数等。

四、例程

下面再看一个例程:

#include <iostream>

// 此重载始终在重载集中
// 省略号形参对于重载决议具有最低等级
void test(...)
{
    std::cout << "调用了保底重载\n";
}

// 如果 C 是类的引用类型且 F 是指向 C 的成员函数的指针,
// 那么这个重载会被添加到重载集
template<class C, class F>
auto test(C c, F f) -> decltype((void)(c.* f)(), void())
{
    std::cout << "调用了引用重载\n";
}

// 如果 C 是类的指针类型且 F 是指向 C 的成员函数的指针,
// 那么这个重载会被添加到重载集
template<class C, class F>
auto test(C c, F f) -> decltype((void)((c->* f)()), void())
{
    std::cout << "调用了指针重载\n";
}

struct X { void f() {} };

int main()
{
    X x;
    test(x, &X::f);
    test(&x, &X::f);
    test(42, 1337);
}

运行结果:

调用了引用重载
调用了指针重载
调用了保底重载

通过例程可以更好的明白SFINAE的用法。这里有一个需要注意的地方就是那个“void test(…)”函数,里面的省略号的用法,对比前面的变参模板和变参函数等,对比性的学习记忆。因为毕竟单独一个使用省略号的函数是无法编译通过的,但是在重载中就可以使用。

五、总结

SFINAE,对于想在模板编程中有更深的想法时,建议还是要认真学习。其实SFINAE更像是模板编程中更进阶的一种用法,虽然它可能在复杂的应用情况有些晦涩难懂,但既然决定想搞c++的模板编程,一些精深的东西还是需要去学习的。特别之于模板的元编程,更是如此。虽然有了concepts,但看以前的代码还是需要懂得这个技术的。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值