C++之SFINAE

1. 关于模板

开始正文之前,先来回忆下c++的模板,目前来看有三种模板形式可以调用,模板类,模板函数以及模板变量。

// 模板类
template<typename T>
struct A {
    T a;
};


// 模板函数
template<typename T>
T add(T t1, T t2) {
    return t1 + t2;
}

// 模板变量
template<typename T = int> //我们写作默认int 
T p{};//初始化列表 为0


void testTpl() {
    // 使用模板类
    A<int> aObj;
    aObj.a = 10;
    std::cout << aObj.a << std::endl;

    // 使用模板函数, 或者add<int>(2, 3)显式指定类型
    std::cout << add(2, 3) << std::endl;

    // 使用模板变量
    p<int> = 666;
    std::cout << p<int> << std::endl;
}

2. 关于SFINAE

先来引入简单的一段代码:

struct X {
    using type = float;
};

struct Y {
    using type2 = float;
};


template <typename T, typename U>
void foo(T t, typename U::type u) {
}

void testSFINAE1() {
    foo<int, X>(5, 5.0); // 可以编译过
    foo<int, Y>(5, 5.0); // 不可以编译过
}

X中有type, Y中有type2,当调用foo时,foo中第二个模板参数匹配的是U的type,我们分别传入X和Y,但是Y中是没有type的,所以猜想下可能会发生什么?

以下是windows下编译的报错:

error C2672: “foo”: 未找到匹配的重载函数

这里报错编译不过,并不是类型匹配失败之类的提示,而是函数没找到。

所有这里抛出结论:

模板参数推导过程中,C++编译器会从多个候选的重载函数中找到一个完美匹配的函数。其中某一个函数匹配失败,不会直接报编译错误,而是继续去寻找下一个可以匹配的函数。这个过程总结就是SFINAE(Substitution Failure Is Not An Error<替换失败不是错误>)。

struct X2 {
    using type1 = int;
};

struct SFINAE2 {
    template<typename T>
    void foo(typename T::type t) {
        std::cout << "SINFAE2 type:" << t << std::endl;
    }

    template<typename T>
    void foo(typename T::type1 t) {
        std::cout << "SINFAE2 type1:" << t << std::endl;
    }
};

void testSFINAE2() {
    SFINAE2().foo<X2>(5); // SINFAE2 type1: 5
}

使用以上例子来正确使用下SFINAE,重载的两个函数foo的模板参数都是T,函数参数分别是T::type和T::type1,这里我们使用X2作为模板参数,假设编译器首先找到第一个foo函数,发现替换(匹配)失败,先不报错而是继续寻找下一个可以匹配的函数,最终找到第二个foo函数并调用。

3. 关于SFINE用法

有了以上的认识,其实我们可以得出可以根据不同类型来在编译器去设定好去调用特定的函数,那看下如何应用:

struct SFINAE3 {
    // 是整数
    template<typename T>
    bool is_odd(T t) {
        return bool(t%2);
    }

    // 不是整数
    template<typename T>
    bool is_odd(T t) {
        return false;
    }
};

给定一个例子,我们查一下这个数是不是奇数,写成这样写编译器无法识别,但是可以利用SFINAE的原理来安排编译器按需识别出相应的函数。

struct SFINAE3 {
    // 是整数
    template<typename T, typename = typename std::enable_if<std::is_integral<T>::value>::type>
    void isOdd(T t) {
        std::cout << "is odd:" << bool(t % 2) << std::endl;
    }

    // 不是整数
    void isOdd(...) {
        std::cout << "not number" << std::endl;
    }
};

void testSFINAE3() {
    SFINAE3().isOdd(2);
    SFINAE3().isOdd(2.4);
    SFINAE3().isOdd("123456");
}

这个例子是如果参数是integral,我们来判断是不是奇数,否则我们输出“not number”。

看到第一个isOdd函数加了一个模板参数,这个模板参数的用处就是用来校验类型,看下enable_if实现:

template <bool _Test, class _Ty = void>
struct enable_if {}; // no member "type" when !_Test

template <class _Ty>
struct enable_if<true, _Ty> { // type is _Ty for _Test
    using type = _Ty;
};

接受两个模板参数,第二个模板参数默认是void类型,当第一个模板参数是true的时候,可以取到enable_if::type,否则无法取得。应用到这里就是如果T这个值是integral,isOdd的第二个模板参数就是void,否则这个函数就会编译失败,继续寻找下一个函数,下一个函数是个万能匹配参数的函数。

可以看到,通过使用以上的定义,我们就可以来根据传递参数的不同类型来进入不同的函数执行。

再来一个稍微复杂的例子充分说明一下:

template<typename T>
class DetectX
{
    struct Fallback { int X; }; // add member name "X"
    struct Derived : T, Fallback { };

    template<typename U, U> struct Check;

    typedef char ArrayOfOne[1];  // typedef for an array of size one.
    typedef char ArrayOfTwo[2];  // typedef for an array of size two.

    template<typename U> 
    static ArrayOfOne & func(Check<int Fallback::*, &U::X> *);
    
    template<typename U> 
    static ArrayOfTwo & func(...);

  public:
    typedef DetectX type;
    enum { value = sizeof(func<Derived>(0)) == 2 };
};

这个类的作用是用来寻找传入的模板参数T中是不是有一个X的成员。我们看下他是如何实现的:

首先他告知外边是否有X的成员是通过枚举的value是否是true来说明的。然后我们看到当sizeof(func(0))等于2是就是可以找到,这里sizeof(func(0))是说调用哪一个func函数,因为我们看到两个func的函数返回值的大小一个是1,一个是2。那也就是如果调用第二个func函数就是能找到T中有X成员。

然后我们他是如果来调用fun函数的,func函数同样是一个模板函数,我们调用的传入Derived就是所谓的U。第一个函数func中使用了Check这个结构体,check的结构体是两个模板参数的,但是这两个模板参数都是一样的,这样就可以使用他来判断了。如果使用check时传入两个模板参数不一致,那可能编译不过。所以我们回到第一个func函数这里来看,他是判断Derived的X成员是不是和Fallback成员是同一个。如果是那么可以编译通过执行第一个func函数,如果不同那就执行第二个func函数。那么Derived和Fallback是什么关系呢,继续向上看,Derived继承了T和Fallback,这样就清晰了,Derived首先继承T,如果T中有X成员,那么Derived中的X应该是来自于T的,否则就是来自于Fallback。来自于Fallback当前就能表明T中没有X。

最后我们看到用来探查一个类中是否有某个成员变量这样的方式很绕,但是也充分利用了SFINAE的原理。

4. 使用concept

C++20引入了一个新的语法特性,concept在我自身的理解就是限制,限制类型,限制某些操作等等。concept可以做到以上我们讲到的内容,而且还更简便。

4.1 concept的定义

先来看下concept如何定义,一般来说std会提供一些定义好的concept供我们使用,我们自己也可以自定义:

template<template-parameter-list>
concept concept-name = constraint-expression;

首先是模板参数的声明,这和我们写模板没甚区别,只不过这个模板是给concept使用,继续concept关键字,后边跟着是你定一个concept名字,

constraint-expression是一个可以被eval为bool的表达式或者编译期函数,可以使用requires来达到效果。

我们看下std的几个声明concept

// 一个永远都能匹配成功的concept
template <typename T>
concept always_satisfied = true; 

// 一个约束T只能是整数类型的concept,整数类型包括 char, unsigned char, short, ushort, int, unsinged int, long等。
template <typename T>
concept integral = std::is_integral_v<T>;

// 一个约束T只能是整数类型,并且是有符号的concept
template <typename T>
concept signed_integral = integral<T> && std::is_signed_v<T>;

然后我们自定义一个使用一个编译期的函数这里需要使用requires关键字:

template <typename T> concept Incrementable = requires (T t) { ++t; }

这个concept表示T类型的对象需要满足可以使用++t这样的操作。

4.2 concept的使用

然后进一步我们看下如何使用concept:(使用concept有很多种写法,这里我写出来我比较喜欢的两种写法)

#include <concepts>
#include <iostream>

template <std::signed_integral T, typename U>
void doDomthing(T t, U u) {
    std::cout << t << u << std::endl;
}

int main()
{
	doDomthing(1, "123");
 	return 0;
}

首先要包含头文件,然后看到模板参数前不再是typename,而是由concept的名字来替换,就表示T这个类型需要满足signed_integral的限制。我们调用时第一个函数参数需要传入有符号的整数。

或者使用一下方式使用concept:

#include <concepts>
#include <iostream>
 
void print(std::integral auto i) {
    std::cout << "Integral: " << i << '\n';
}
 
void print(auto x) {
    std::cout << "Non-integral: " << x << '\n';
}
 
int main()
{
    print(1); // Integral: 1
    print("123"); // Non-integral: 123
}

第一个print的函数中是直接把concept的名字写到参数那里,然后后边跟着一个auto。同样表示i这个参数需要满足是个整数的形式。

最后我们再来看下如何使用concept来写一下检查类中是否有X的功能:

#include <concepts>
#include <iostream>

struct AA {
   int X = 10;
};

template<typename T>
concept Num = std::is_integral_v<decltype(T::X)> && requires (T t) {
    t.X;
};

template<Num T>
void print(T t) {
    std::cout << "Has: " << t.X << '\n';
}

void print(auto x) {
    std::cout << "Non: " << x << '\n';
}

int main()
{
    AA a;

    print(a); // Has: 10
    print(1); // Non: 1
}

我们声明一个AA结构体来测试,我们自定义一个concept来判断,判断T的X类型是否是整数,并且判断可以使用T的对象t访问到X。其实可以直接使用&&后边的条件就可以,这里只是为了展示,这两种顶一个concept的方式可以同时使用。做好了限定后,我们使用Num来限制函数达到了根据不同类型调用不同的函数。

5.总结

这里我们就结束了,首先我们简单复习了一下模板,然后我们讲述了SFINAE的概念和用法,最后我们看如果用concept来实现出来SFINAE这个类似的功能。

6. ref

https://en.wikibooks.org/wiki/More_C++_Idioms/Member_Detector

https://en.wikibooks.org/wiki/More_C%2B%2B_Idioms/SFINAE

https://github.com/wuye9036/CppTemplateTutorial

https://zhuanlan.zhihu.com/p/107610017

https://cpp.sh/

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值