【C++】C++11右值引用|新增默认成员函数|可变参数模版|lambda表达式(下)

简介: 【C++】C++11右值引用|新增默认成员函数|可变参数模版|lambda表达式(下)

3. 可变参数模版


我们之前了解到模板的概念,让我们的代码中类和函数都可以模板化,从而支持多种不同类型。但是在C++98/03中,类模板和函数模板的参数只能是固定数量的,但是在C++11中,出现了可变模板参数,让模板参数能够接收不同数量的参数。

关于可变参数模板,这里只学习一些基本的特性,了解即可。想要深入了解的小伙伴可以自行查找资料。


3.1 可变参数模板的语法

对于可变参数,其实在我们刚开始学习C语言的时候就已经使用可变参数的函数了,对,就是printf函数

d0f892f864a028f8c6fd877b4d5b3ac8.png

看到printf函数原型中用...表示可变参数,C++11也采用了类似的方法,我们看下面一个C++的可变参数的函数模板

template<class ...Args>
void ShowList(Args... args)
{}


注意:这里的Args是一个模板参数包,args是一个函数形参的参数包,这个参数包中可以包含0-N个参数


上面的参数args前面有省略号,所以它就是一个可变模版参数,我们把带省略号的参数称为“参数包”,它里面包含了0到N(N>=0)个模版参数。

获取参数包中参数个数的方式只有一种:使用sizeof关键字

ed4fb1d02799edad7e4879ca2be5cdde.png

这里有一个点需要注意:我们需要将代表参数包的...放在sizeof的括号外面,不要思考这个用法的逻辑,当作新的语法记住就好。


但是这里有一个问题,我们无法直接获取参数包args中的每个参数的,只能通过展开参数包的方式来获取参数包中的每个参数,这是使用可变模版参数的一个主要特点,也是最大的难点,即如何展开可变模版参数。由于语法不支持使用args[i]这样方式获取可变参数,所以我们的用一些奇招来获取参数包的值。


3.2 递归函数方式展开参数包

这里我们主要利用的就是参数包里的参数个数可以是任意个,所以设计一个重载的函数用来当作递归的出口

//递归出口
template<class T>
void ShowList(const T& t)
{
    cout << t << endl;
}
//递归过程
template<class T, class ...Args>
void ShowList(T value, Args... args)
{
    cout << value << " ";
    ShowList(args...);//这里要使用...表示将参数包展开
}
void Test10()
{
    ShowList(1);
    ShowList(1, 'A');
    ShowList(1, 'A', string("sort"));
}


这里如果参数包里之后一个参数的话,就调用递归出口,如果大于一个参数,那么就会调用递归过程,然后将第一个参数识别给T,剩下的参数都放进从参数包中,递归调用。

ed9a226d6ad378e3c9c8167648466912.png


3.3 逗号表达式展开参数包

template<class T>
void PrintArgs(T t)
{
    cout << t << " ";
}
template<class... Args>
void ShowList(Args... args)
{
    int arr[] = { (PrintArgs(args), 0)... };
    cout << endl;
}
void Test10()
{
    ShowList(1);
    ShowList(1, 'A');
    ShowList(1, 'A', string("sort"));
}


这种展开参数包的方式,不需要通过递归终止函数,是直接在expand函数体中展开的, PrintArg不是一个递归终止函数,只是一个处理参数包中每一个参数的函数。这种就地展开参数包的方式实现的关键是逗号表达式。我们知道逗号表达式会按顺序执行逗号前面的表达式。

expand函数中的逗号表达式:(printarg(args), 0),也是按照这个执行顺序,先执行PrintArg(args),再得到逗号表达式的结果0。


同时还用到了C++11的另外一个特性——初始化列表,通过初始化列表来初始化一个变长数组, {(printarg(args), 0)…}将会展开成((printarg(arg1),0),(printarg(arg2),0), (printarg(arg3),0), etc… ),最终会创建一个元素值都为0的数组int arr[sizeof…(Args)]。由于是逗号表达式,在创建数组的过程中会先执行逗号表达式前面的部分printarg(args)打印出参数,也就是说在构造int数组的过程中就将参数包展开了,这个数组的目的纯粹是为了在数组构造的过程展开参数包。


3.4 可变参数模板在STL中的应用——empalce相关接口函数

在增加了可变参数模板的语法之后,STL也增加了对应的接口,这里我们看一下vector中的emplace。

273b6fa0a74a423631177d940524a56a.png

首先我们看到的emplace系列的接口,支持模板的可变参数,并且万能引用。那么相对insert和emplace系列接口的优势到底在哪里呢?


  • 对于内置类型来说,emplace 接口和传统的插入接口在效率上是没有区别的,因为内置类型是直接插入的,不需要进行拷贝构造;


  • 对于需要进行深拷贝的自定义类型来说,如果该类实现了移动构造,则 emplace 接口会比传统插入接口少一次浅拷贝,但总体效率差不多;如果该类没有实现移动构造,则 emplace 接口的插入效率要远高于传统插入接口;


  • 这是因为在传统的插入接口中,需要先创建一个临时对象,然后将这个对象深拷贝或者移动拷贝到容器中,而 std::emplace() 则通过使用可变参数模板、万能模板等技术,直接在容器中构造对象,避免了对象的拷贝和移动;


  • 对于不需要进行深拷贝的自定义类型来说,emplace 接口也会比传统插入接口少一次浅拷贝 (拷贝构造),但总体效率也差不多;原因和上面一样,emplace 接口可以直接在容器中原地构造新的对象,避免了不必要的拷贝过程


在上一篇文中我们讲到,emplace 接口要比传统的插入接口高效,我们能使用 emplace 就不要使用传统插入接口,严格意义上说这种说法没有问题,但是并不是绝对的;因为 STL 中的容器都支持移动构造,所以 emplace 接口仅仅是少了一次浅拷贝而已,而浅拷贝的代价并不大;所以我们在使用 STL 容器时并不需要去刻意的使用 emplace 系列接口。


注意:上面的传统接口的移动构造和 emplace 接口的直接在容器中构造对象都只针对右值 (将亡值),而对于左值,它都只能老实的进行深拷贝


4. lambda表达式


在C++11之前,我们想要使用sort排序,使用sort,需要传仿函数用于规定比较的原则

4ef3195f4defb072ccd2f6b88192c729.png

可以看到,如果想要排序内置类型,可以使用仿函数greater/less,但是如果要排序自定义类型,就得自己写仿函数。特别是如果遇到同一个类按照不同方式排序的情况,就要去实现不同的类,特别是相同类的命名,给开发者带来了极大的不便,因此C++11中引进了Lambda表达式

首先我们见一见什么叫lambda表达式

struct Goods
{
    string _name; // 名字
    double _price; // 价格
    int _evaluate; // 评价
    Goods(const char* str, double price, int evaluate)
        :_name(str)
        , _price(price)
        , _evaluate(evaluate)
    {}
};
void Test12()
{
    //这里想对存放的Goods对象按照不同的方式进行排序,就可以使用lambda表达式
    vector<Goods> v = { {"apple",2.1,5},{"banana",3,4},{"orange",2.2,3}, {"pineapple",1.5,4 } };
    sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {
        return g1._name < g2._name; });
    cout << "sort by name" << endl;
    for (auto& e : v)
    {
        cout << e._name << " " << e._price << " " << e._evaluate << endl;
    }
    cout << endl;
    sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {
        return g1._price < g2._price; });
    cout << "sort by price" << endl;
    for (const auto& e : v)
    {
        std::cout << e._name << " " << e._price << " " << e._evaluate << endl;
    }
    cout << endl;
    sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {
        return g1._evaluate < g2._evaluate; });
    cout << "sort by evaluate" << endl;
    for (const auto& e : v)
    {
        cout << e._name << " " << e._price << " " << e._evaluate << endl;
    }
    cout << endl;
}


713ff3968a8676916230d98895f2556d.png


4.1 Lambda表达式的语法与用法

lambda表达式书写格式

[capture-list] (parameters) mutable -> return-type { statement}


表达式说明:

  • [capture-list]:捕捉列表。出现在lambda函数的开始位置,编译器根据[]来判断接下来的代码是否是lambda表达式,因此此项不能省略,捕捉列表能够捕捉上下文中的变量共lambda表达式使用。
  • (parameters):参数列表。与普通函数的参数列表一致,如果不需要参数传递,可以连同()一起省略。
  • mutable:默认情况下,lambda表达式(函数)总是一个const函数,mutable可以取消其常性。
  • 注:使用此修饰符的时候,参数列表不可省略
  • ->return-type:返回值类型。用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可以省略。返回值类型明确的情况下也可以省略,由编译器对返回类型进行推导。
  • {statement}:函数体。在函数体内部,除了可以使用(parameters)中的函数参数外,还可以使用[capture-list]捕捉到的变量。


根据上述的语法格式,我们知道参数列表和返回值类型都是可选部分,而捕捉列表和函数体可以为空

void Test13()
{
    // 最简单的lambda表达式,没有任何实际意义
    [] {};
    // 省略参数列表和返回值类型,返回值类型由编译器推导为int
    int a = 3, b = 4;
    [=] {return a + 3; };
    // 省略了返回值类型,无返回值类型
    auto fun1 = [&](int c) {b = a + c; };//由于lambda表达式的类型是编译器自动生成的,非常复杂,所有我们使用auto来定义
    fun1(10);
    cout << a << " " << b << endl;
    // 各部分都很完善的lambda函数
    auto fun2 = [=, &b](int c)->int {return b += a + c; };
    cout << fun2(10) << endl;
    // 赋值捕捉x
    int x = 10;
    auto add_x = [x](int a) mutable {
        x *= 2;
        return a + x; };
    cout << add_x(10) << endl;
}


捕捉列表说明


捕捉列表描述了上下文中哪些数据可以被lambda使用,以及使用的方式传值还是传引用


  • [var]:表示传值的方式捕捉变量var
  • [=]:表示传值的方式捕捉所有父作用域的变量(包括this)
  • [&var]:表示引用捕捉变量var
  • [&]:表示引用捕捉所有父作用域的变量(包括this)
  • [this]:标志值传递的方式捕捉当前的this指针


注意


  1. 父作用域指包含lambda函数的语句块
  1. 语法上捕捉列表可以由多个捕捉项组成,并以逗号分隔
  1. 例如:[=, &a, &b]:表示以引用传递的放啊是捕捉a和b,值传递方式捕捉其他所有变量
  1. [&, a, this]:值传递的方式捕捉a和this,引用方式捕捉其他所有变量
  1. 捕捉列表不允许变量重复传递,否则就会导致编译错误
  1. 在第2点上我们见到了[=, &a]这种用法,默认按照值传递的方式捕捉了所有的变量,但是把a单独拿出来说,意思将a单独处理,按照引用的方式传递。但是如果这里换成[=, a],就出现了重复传递,会导致编译错误。
  1. 在块作用域以外的lambda函数捕捉列表必须为空
  1. 在块作用域中的lambda函数仅能捕捉父作用域中局部变量,捕捉任何非此作用域或者非局部变量都会导致编译报错
  1. lambda表达式之间不能相互赋值,及时看起来类型相同

69e28a0f6b24b72a5ce04db81a588be9.png

4.2 Lambda表达式的底层原理

实际上编译器在底层对lambda表达式的处理方式,是转换成函数对象(仿函数)再处理的。所谓仿函数,就是在类中重载了operator()运算符。我们看下面一段代码

class Add
{
public:
    Add(int base)
        :_base(base)
    {}
    int operator()(int num)
    {
        return _base + num;
    }
private:
    int _base;
};
void Test15()
{
    int base = 1;
    //仿函数的调用方式
    Add add1(base);//构造一个函数对象
    cout << add1(10) << endl;
    //lambda表达式
    auto add2 = [base](int num)->int 
    {
        return base + num; 
    };
    cout << add2(10) << endl;
}


这里定义了一个Add类,在其中进行了operator()的重载,然后实例化出了add1就可以叫做函数对象,可以像函数一样使用。然后定义了add2,这里将lambda表达式赋值给add2,此时add1和add2都可以像函数一样使用。

接下来我们看一下汇编的情况

d3cdb4bc69b783cd440dcfd5f9ee715c.png

我们首先看汇编语言中的1,可以看到在使用函数对象的时候,调用了Add中的operator()。

看2,这里是使用lambda表达式对add2进行赋值和调用,可以看到调用的同样也是operator()函数。值得注意的是这里调用的是<lambda_1>中的operator(),本质就是lambda表达式在底层被转换成了仿函数。

❓为什么我们不能显示的写lambda表达式的类型?

✅因为这个类型是由编译器自动生成的,我们没办法知道这个类名称的具体写法。

在VS下,生成的这类的类名叫做lambda_uuid,类名中的uuid叫做通用唯一识别码(Universally Unique Identifier),简单来说,uuid就是通过算法生成一串字符串,保证在当前程序当中每次生成的uuid都不会重复,这样就能保证每个lambda表达式底层类名都是唯一的。

(img-indD9wQt-1690376726287)]


4.2 Lambda表达式的底层原理

实际上编译器在底层对lambda表达式的处理方式,是转换成函数对象(仿函数)再处理的。所谓仿函数,就是在类中重载了operator()运算符。我们看下面一段代码

class Add
{
public:
    Add(int base)
        :_base(base)
    {}
    int operator()(int num)
    {
        return _base + num;
    }
private:
    int _base;
};
void Test15()
{
    int base = 1;
    //仿函数的调用方式
    Add add1(base);//构造一个函数对象
    cout << add1(10) << endl;
    //lambda表达式
    auto add2 = [base](int num)->int 
    {
        return base + num; 
    };
    cout << add2(10) << endl;
}


这里定义了一个Add类,在其中进行了operator()的重载,然后实例化出了add1就可以叫做函数对象,可以像函数一样使用。然后定义了add2,这里将lambda表达式赋值给add2,此时add1和add2都可以像函数一样使用。


接下来我们看一下汇编的情况


[外链图片转存中…(img-LEIRNSC7-1690376726288)]


我们首先看汇编语言中的1,可以看到在使用函数对象的时候,调用了Add中的operator()。


看2,这里是使用lambda表达式对add2进行赋值和调用,可以看到调用的同样也是operator()函数。值得注意的是这里调用的是<lambda_1>中的operator(),本质就是lambda表达式在底层被转换成了仿函数。


❓为什么我们不能显示的写lambda表达式的类型?


✅因为这个类型是由编译器自动生成的,我们没办法知道这个类名称的具体写法。


在VS下,生成的这类的类名叫做lambda_uuid,类名中的uuid叫做通用唯一识别码(Universally Unique Identifier),简单来说,uuid就是通过算法生成一串字符串,保证在当前程序当中每次生成的uuid都不会重复,这样就能保证每个lambda表达式底层类名都是唯一的。


本节完 …

相关文章
|
5月前
|
程序员 编译器 C++
【实战指南】C++ lambda表达式使用总结
Lambda表达式是C++11引入的特性,简洁灵活,可作为匿名函数使用,支持捕获变量,提升代码可读性与开发效率。本文详解其基本用法与捕获机制。
212 49
|
8月前
|
编译器 C++ 容器
【c++11】c++11新特性(上)(列表初始化、右值引用和移动语义、类的新默认成员函数、lambda表达式)
C++11为C++带来了革命性变化,引入了列表初始化、右值引用、移动语义、类的新默认成员函数和lambda表达式等特性。列表初始化统一了对象初始化方式,initializer_list简化了容器多元素初始化;右值引用和移动语义优化了资源管理,减少拷贝开销;类新增移动构造和移动赋值函数提升性能;lambda表达式提供匿名函数对象,增强代码简洁性和灵活性。这些特性共同推动了现代C++编程的发展,提升了开发效率与程序性能。
350 12
|
10月前
|
存储 机器学习/深度学习 编译器
【C++终极篇】C++11:编程新纪元的神秘力量揭秘
【C++终极篇】C++11:编程新纪元的神秘力量揭秘
|
算法 编译器 C++
【C++11】lambda表达式
C++11 引入了 Lambda 表达式,这是一种定义匿名函数的方式,极大提升了代码的简洁性和可维护性。本文详细介绍了 Lambda 表达式的语法、捕获机制及应用场景,包括在标准算法、排序和事件回调中的使用,以及高级特性如捕获 `this` 指针和可变 Lambda 表达式。通过这些内容,读者可以全面掌握 Lambda 表达式,提升 C++ 编程技能。
573 3
|
存储 安全 C++
【C++11】右值引用
C++11引入的右值引用(rvalue references)是现代C++的重要特性,允许更高效地处理临时对象,避免不必要的拷贝,提升性能。右值引用与移动语义(move semantics)和完美转发(perfect forwarding)紧密相关,通过移动构造函数和移动赋值运算符,实现了资源的直接转移,提高了大对象和动态资源管理的效率。同时,完美转发技术通过模板参数完美地转发函数参数,保持参数的原始类型,进一步优化了代码性能。
208 2
|
10月前
|
编译器 C++ 开发者
【C++篇】深度解析类与对象(下)
在上一篇博客中,我们学习了C++的基础类与对象概念,包括类的定义、对象的使用和构造函数的作用。在这一篇,我们将深入探讨C++类的一些重要特性,如构造函数的高级用法、类型转换、static成员、友元、内部类、匿名对象,以及对象拷贝优化等。这些内容可以帮助你更好地理解和应用面向对象编程的核心理念,提升代码的健壮性、灵活性和可维护性。
|
6月前
|
人工智能 机器人 编译器
c++模板初阶----函数模板与类模板
class 类模板名private://类内成员声明class Apublic:A(T val):a(val){}private:T a;return 0;运行结果:注意:类模板中的成员函数若是放在类外定义时,需要加模板参数列表。return 0;
192 0
|
6月前
|
存储 编译器 程序员
c++的类(附含explicit关键字,友元,内部类)
本文介绍了C++中类的核心概念与用法,涵盖封装、继承、多态三大特性。重点讲解了类的定义(`class`与`struct`)、访问限定符(`private`、`public`、`protected`)、类的作用域及成员函数的声明与定义分离。同时深入探讨了类的大小计算、`this`指针、默认成员函数(构造函数、析构函数、拷贝构造、赋值重载)以及运算符重载等内容。 文章还详细分析了`explicit`关键字的作用、静态成员(变量与函数)、友元(友元函数与友元类)的概念及其使用场景,并简要介绍了内部类的特性。
294 0
|
9月前
|
设计模式 安全 C++
【C++进阶】特殊类设计 && 单例模式
通过对特殊类设计和单例模式的深入探讨,我们可以更好地设计和实现复杂的C++程序。特殊类设计提高了代码的安全性和可维护性,而单例模式则确保类的唯一实例性和全局访问性。理解并掌握这些高级设计技巧,对于提升C++编程水平至关重要。
193 16
|
10月前
|
编译器 C语言 C++
类和对象的简述(c++篇)
类和对象的简述(c++篇)