Bootstrap

C++11可变参数模板,lambda表达式,包装器

目录

可变参数模板

lambda表达式

问题的引入

lambda表达式语法

捕捉列表的使用

函数对象和lambda表达式

function包装器


 

可变参数模板

C++11的新特性可变参数模板能够让您创建可以接受可变参数的函数模板和类模板,相比C++98/,类模版和函数模版中只能含固定数量的模版参数,可变模版参数无疑是一个巨大的改进。然而由于可变模版参数比较抽象,使用起来需要一定的技巧,所以这块还是比较晦涩的。现阶段呢,我们掌握一些基础的可变参数模板特性就够我们用了。

//Args是一个模板参数包,args是一个函数形参参数包
//声明一个参数包Args ...  args,这个参数包可以包括0到任意个模板参数.
template<class ...Args>
void ShowList(Args ...  args)
{}

以上代码代表ShowList可以有0到任意个模板参数.

int main()
{
    string str = "hello";
    ShowList(1);
    ShowList(1, 'A');
    ShowList(1, 'A', str);
}

这样写都可以,编译器会自动推导出变量的类型。
上面的参数args前面有省略号,所以它就是一个可变模版参数,我们把带省略号的参数称为“参数包”,它里面包含了0到N(N>=0)个模版参数。我们无法直接获取参数包args中的每个参数的,只能通过展开参数包的方式来获取参数包中的每个参数,这是使用可变模版参数的一个主要特点,也是最大的难点,即如何展开可变模版参数。由于语法 这样方式获取可变参数,所以我们的用一些特殊的方法来获取参数包的值。

我们首先可以根据sizeof...(args)算出参数包的 个数。

但是参数包里面的内容该如何取出来呢?

有两种方法,这里给出一种取出的方法.

//递归终止
void ShowList()
{
    cout <<  endl;
}
// 展开函数
template <class T, class ...Args>
void ShowList(const T& value, Args... args)
{
    cout << value << " ";
    ShowList(args...);
}

它的思路是:增加了一个模板参数T,每次传入参数包时,第一个参数总是T,然后取出来,把参数包中剩余的再作为一个参数包传给自身,相当于递归调用。当参数为空的时候,说明已经全部取完,会自动调用合适的函数,即无参的函数。

我们运行一下:

可以看到已经全部取出来了.

lambda表达式

问题的引入

在C++98中,我们如果想对一个数组中的元素进行排序,可以进行如下操作:

int main()
{
    int array[] = { 4,1,8,5,3,7,0,9,2,6 };
    // 默认按照小于比较,排出来结果是升序
    std::sort(array, array + sizeof(array) / sizeof(array[0]));
    // 如果需要降序,需要改变元素的比较规则
    std::sort(array, array + sizeof(array) / sizeof(array[0]), greater<int>());
    return 0;
}

这里说句题外话:我们发现greater<int>后面这个括号有时候加,有时候又不加。那我们该如何分辨呢?

其实如果传入的是对象的话,就需要加上(),毕竟需要构造一个匿名对象来使用。

   sort(arr.begin(), arr.end(), greater<int>());

而如果需要传入的是类型的话,则不需要加(),直接greater<int>即可,例如优先级队列:

    priority_queue<int, vector<int>, greater<int>>;

如果待排序元素为自定义类型,需要用户定义排序时的比较规则。

struct Goods
{
    string _name;
    double _price;
    int _evaluate;
};
struct Compare
{
    bool operator()(const Goods& gl, const Goods& gr)
    {
        return gl._price <= gr._price;
    }
};
int main()
{
    vector<Goods> gds{ { "苹果", 2.1 }, { "相交", 3 }, { "橙子", 2.2 }, {"菠萝", 1.5} };
    sort(gds.begin(),gds.end(), Compare());
    return 0;
}

但是以上只是一种比较写法,只是按价格大的进行比较,一共有3个属性,如果我们都想用的话,得写6个仿函数,会显得非常麻烦。

所以这个时候我们需要用lambda表达式了.

lambda表达式语法

lambda表达式书写格式:[capture-list] (parameters) mutable -> return-type { statement }
1. lambda表达式各部分说明
[capture-list] : 捕捉列表,该列表总是出现在lambda函数的开始位置,编译器根据[]来判断接下来
的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量供lambda函数使用。

(parameters):参数列表。与普通函数的参数列表一致,如果不需要参数传递,则可以连同()一起
省略
mutable:默认情况下,lambda函数总是一个const函数,mutable可以取消其常量性。使用该修
饰符时,参数列表不可省略(即使参数为空)。

->returntype:返回值类型。追踪返回类型形式声明函数的返回值类型,没有返回值时此部分
可省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推导。
{statement}:函数体。在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量。

注意: 在lambda函数定义中,参数列表和返回值类型都是可选部分,而捕捉列表和函数体可以为空。因此C++11中最简单的lambda函数为:[]{}; 该lambda函数不能做任何事情。

看一个比较简单的lambda例子:

    //两个数相加的lambda 
    [](int a, int b)->int {return a + b; };

那我们该怎么使用这个式子呢,由于lambda函数是一个匿名的函数。

此时auto就发挥作用,用auto一个变量来接收它.

    auto add1 = [](int a, int b)->int {return a + b; };

拿到之后,我们就该调用一下,调用方法如下:

    cout << add1(1, 2) << endl;

就和正常函数调用一样,我们运行:

答案是3,也符合我们的预期。

当然因为返回值编译器会自己推导,我们也可以忽略返回值,这样写:

 

    auto add2 = [](int a, int b) {return a + b;  };

接下来,我们写一个交换变量的lambda.

    int x = 1, y = 0;
    //格式不一定一定要写在一行,可以多行分开
    auto swap1 = [](int& x1, int& x2) {
        int tmp = x1;
        x1 = x2;
        x2 = tmp;
    };
    swap1(x, y);
    cout << "x :" << x << "   " << " y :" << y << endl;

可以发现运行结果也正确了。

但是如果我们不传参数,就交换x和y呢 ?

捕捉列表的使用

这里就用到了捕捉列表:

    int x = 1, y = 0;
    //这里捕捉x和y,然后进行操作
    auto swap2 = [x, y] {
        int tmp = x;
        x = y;
        y = tmp;
    };

那以上这么写可以吗?不可以的,被捕捉的对象不能被修改

所以这个时候需要用mutable.用mutable记住一定要在前加上().

所以应该改成如下这样:

    int x = 1, y = 0;
    auto swap2 = [x, y]()mutable {
        int tmp = x;
        x = y;
        y = tmp;
    };

但是我们调用它,然后输出一下结果:

发现x和y并没有交换.其实是因为:

mutable在这里仅仅是让捕捉的对象可以被修改,但并不是说修改后会影响外面的

也就是相当于只是一份形参,改动不会影响外面的实参.

那我们就是真正想改变捕捉到的外面的对象该怎么办?改成传引用捕捉即可.

    auto swap3 = [&x, &y]{
        int tmp = x;
        x = y;
        y = tmp;
    };

在x和y前面加上&符号。

这样便成功交换了外面捕捉到的对象.

了解了这个,我们再回到一开始那个情景,这样我们就不再需要写6个仿函数了,而是可以这么写:

 

struct Goods
{
    string _name;
    double _price;
    int _evaluate;
};
int main()
{
    vector<Goods> gds{ { "苹果", 2.1 }, { "相交", 3 }, { "橙子", 2.2 }, {"菠萝", 1.5} };
    sort(gds.begin(), gds.end(), [](const Goods& g1, const Goods& g2) {
        return g1._price <= g2._price; });
    return 0;
}

而且哪个属性和哪个属性比较也一目了然。当然需要别的,可以直接改lambda函数里面的内容即可.

   sort(gds.begin(), gds.end(), [](const Goods& g1, const Goods& g2) {
        return g1._name <= g2._name; });
   sort(gds.begin(), gds.end(), [](const Goods& g1, const Goods& g2) {
        return g1._evaluate <= g2._evaluate; });

 当然捕捉列表还有以下的用法,有兴趣同学可以自行了解.

2. 捕获列表说明
捕捉列表描述了上下文中那些数据可以被lambda使用,以及使用的方式传值还是传引用。
[var]:表示值传递方式捕捉变量var
[=]:表示值传递方式捕获所有父作用域中的变量(包括this)
[&var]:表示引用传递捕捉变量var
[&]:表示引用传递捕捉所有父作用域中的变量(包括this)
[this]:表示值传递方式捕捉当前的this指针
注意:
a. 父作用域指包含lambda函数的语句块
b. 语法上捕捉列表可由多个捕捉项组成,并以逗号分割。
比如:[=, &a, &b]:以引用传递的方式捕捉变量a和b,值传递方式捕捉其他所有变量。

[&,a, this]:值传递方式捕捉变量a和this,引用方式捕捉其他变量

c. 捕捉列表不允许变量重复传递,否则就会导致编译错误。 比如:[=, a]:=已经以值传递方式捕捉了所有变量,捕捉a重复
d. 在块作用域以外的lambda函数捕捉列表必须为空。
e. 在块作用域中的lambda函数仅能捕捉父作用域中局部变量,捕捉任何非此作用域或者非局部变量都会导致编译报错。
f. lambda表达式之间不能相互赋值,即使看起来类型相同.(lambda --->lambda_uuid)

函数对象和lambda表达式

函数对象,又称为仿函数,即可以像函数一样使用的对象,就是在类中重载了operator()运算符的类对象。

class Rate
{
public:
    Rate(double rate) : _rate(rate)
    {}
    double operator()(double money, int year)
    {
        return money * _rate * year;
    }
private:
    double _rate;
};
int main()
{
    // 函数对象
    double rate = 0.49;
    Rate r1(rate);
    r1(10000, 2);
    // lambda
    auto r2 = [=](double monty, int year)->double {return monty * rate * year; };
    r2(10000, 2);
    return 0;
}

从使用方式上来看,函数对象与lambda表达式完全一样。
函数对象将rate作为其成员变量,在定义对象时给出初始值即可,lambda表达式通过捕获列表可以直接将该变量捕获到。

 实际在底层编译器对于lambda表达式的处理方式,完全就是按照函数对象的方式处理的,即:如果定义了一个lambda表达式,编译器会自动生成一个类,在该类中重载了operator()。

lambda_xxxxx,后面这一串乱码是uuid,是用来标识唯一,每个lambda表达式都有自己的标识,所以即使类型相同,也不能相互赋值.

function包装器

function包装器也叫作适配器。C++中的function本质是一个类模板,也是一个包装器。那么我们来看看,我们为什么需要function呢?
看一下代码:

#include<functional>

template<class F, class T>
T useF(F f, T x)
{
    static int count = 0;
    cout << "count:" << ++count << endl;
    cout << "count:" << &count << endl;
    return f(x);
}
double f(double i)
{
    return i / 2;
}
struct Funtor
{
    double operator()(double d)
    {
        return d / 3;
    }
};

然后看测试代码:

int main()
{
    cout << useF(f, 11.11) << endl;
    cout << useF(Funtor(), 11.11) << endl;
    cout << useF([](double d)->double {return d / 4; }, 11.11) << endl;
    return 0;
}

调用了3词useF,T是相同的,而F不同,每次都会被重新实例化成不同类型的对象,可是useeF函数里的count变量是static全局的啊,这样会被实例化成3份吗?我们取地址即可知道:

可以发现f也被实例化成了3份,但我们就是想要1份,该怎么办呢?

这里就需要用到包装器:

#include<functional>
int f(int a, int b)
{
    return a+b;
}
struct Funtor
{
    int operator()(int a, int b)
    {
        return a+b;
    }
};

class  Puls
{
public:
    static int pulsi(int x, int y)
    {
        return x + y;
    }
    double pulsd(double x, double y)
    {
        return x + y;
    }
};

int main()
{
    function<int(int, int)> f1 = f;//包装普通函数
    cout << f1(2, 3) << endl;
    function<int(int, int)> f2 = Funtor();//包装仿函数
    cout << f2(1, 2) << endl;
    function<int(int, int)> f3 = &Puls::pulsi; //包装静态成员函数可以不加&,但是最好加上
    cout << f3(1, 2) << endl;

    function<double(Puls, double, double)> f4 = &Puls::pulsd;包装非静态函数需要增加参数,但是可以通过band来取消这个参数

    f4(Puls(), 1.1, 2.2);

    return 0;
}

这样包装之后,我们便可以解决生成多份的问题了.

function是一个通用的函数封装类,可以包装不同类型的可调用对象(如函数、函数指针、lambda 表达式等),以及它们的参数和返回值类型。它提供了一种统一的方式来处理函数对象,使得函数的类型和参数在运行时确定。

#include<functional>

template<class F, class T>
T useF(F f, T x)
{
    static int count = 0;
    cout << "count:" << ++count << endl;
    cout << "count:" << &count << endl;
    return f(x);
}
double f(double i)
{
    return i / 2;
}
struct Funtor
{
    double operator()(double d)
    {
        return d / 3;
    }
};
int main()
{
    function<double(double)> f1 = f;  //包装普通函数
    cout << useF(f1, 11.11) << endl;
    function<double(double)> f2 = Funtor();  //包装仿函数
    cout << useF(f2, 11.11) << endl;
    function<double(double)> f3 = [](double d)->double {return  d / 4; }; //包装lamdba表达式
    cout << useF(f3, 11.11) << endl;
    return 0;
}

这样我们再输出出来:

可以看到地址已经一样了,说明只生成了一份了. 这个不是重点,这个是样例。

有的地方必须要求是统一类型,这个时候便需要包装器.

function包装器的大体内容就是这样了.

;