当前位置: 首页 > news >正文

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

文章目录

  • 1. 右值引用和移动语义
    • 1.1 左值引用和右值引用
    • 1.2 左值引用和右值引用的比较
    • 1.3右值引用的使用场景和意义
    • 1.4 左值引用和右值引用的深入使用场景分析
    • 1.5 完美转发
      • 1.5.1 万能引用
      • 1.5.2 完美转发
  • 2. 新的类功能
    • 2.1 默认成员函数
    • 2.2 类成员变量初始化
    • 2.3 强制生成默认函数的关键字defaule
    • 2.4 禁止生成默认函数的关键字delete
    • 2.5继承和多态中的final与override关键字
  • 3. 可变参数模版
    • 3.1 可变参数模板的语法
    • 3.2 递归函数方式展开参数包
    • 3.3 逗号表达式展开参数包
    • 3.4 可变参数模板在STL中的应用——empalce相关接口函数
  • 4. lambda表达式
    • 4.1 Lambda表达式的语法与用法
    • 4.2 Lambda表达式的底层原理
    • 4.2 Lambda表达式的底层原理

1. 右值引用和移动语义

1.1 左值引用和右值引用

在C++11之前,我们只有引用的概念,没有接触到所谓的左值引用或者是右值引用这种概念,从C++11开始,增加了右值引用的概念,那么现在我们将对引用进行一个概念上的区分。在此之前我们所说的引用都是左值引用,对于左值引用相关的内容,可以去看一看博主之前写的文章C++引用。

不管是左值引用还是右值引用,本质上都是给对象取别名

那么,怎么区别左值引用和右值引用呢?

左值是一个表示数据的表达式(一个变量名或者是解引用的指针),我们可以获取到他的地址+可以对它进行赋值左值可以在等号的左边,右值不能在等号的左边,const修饰的变量不能被赋值,但是能够取地址。

右值也是一个表达式,如:字面量,表达式的返回值,函数的返回值,右值能够出现在赋值符号的右边,不能出现在赋值符号的左边,右值不能取地址

左值引用就是对左值进行取别名操作,右值引用就是对右值取别名

void Test1()
{//左值int a = 1;double x = 1.1, y = 2.2;int* pb = new int(10);const int c = 2;//左值引用int& ra = a;int*& rpb = pb;const int& rc = c;int& pvalue = *pb;//右值10;x + y;min(x, y);//右值引用int&& rr1 = 10;int&& rr2 = x + y;int&& rr3 = min(x, y);
}

一个有趣的现象:

我们知道,右值是不能被赋值的,但是看下面这段代码

void Test2()
{int x = 1, y = 2;int&& rr1 = x + y;cout << "x + y:" << x + y << endl;cout << "rr1:" << rr1 << endl;rr1 = 10;cout << "rr1" << rr1 << endl;
}

右值x+y在被右值引用之后就可以被赋值了,即变成了左值

这是因为在给右值取别名之后,会被存储在一个特定的位置,然后就能取到该位置的地址,因此也就能更改次地址存放的值。如果不想让它能被能改就可以使用const修饰右值引用。当然实际应用中不会使用到这个特性,所以这个特性也就不重要

1.2 左值引用和右值引用的比较

左值引用的总结:

  • 左值引用只能引用左值,不能引用右值
  • const左值引用既能引用左值,也能引用右值(这个跟我们之前所说的临时变量具有常性可以对照,那个临时变量就是右值)
void Test3()
{//左值引用只能引用左值,不能引用右值int a = 10;int& ra1 = a;//int& ra2 = 10;//右值引用不能引用左值,因此这行代码报错//const左值引用既能引用左值,也能引用右值const int& ra3 = 10;//引用右值const int& ra4 = a;//引用左值
}

右值引用的总结:

  • 右值引用只能引用右值,不能引用左值
  • move函数可以将左值变为右值,因此右值引用可以引用move之后的左值
void Test4()
{//右值引用只能引用右值,不能引用左值int&& r1 = 10;int a = 10;//int&& r2 = a;//右值引用引用左值报错//Xcode报错内容:Rvalue reference to type 'int' cannot bind to lvalue of type 'int'(无法将左值绑定到右值引用)//右值引用能引用move之后的左值int&& r3 = std::move(a);
}

1.3右值引用的使用场景和意义

既然在C++11之前已经有了左值引用,为什么还要加上右值引用这种概念呢?不是“画蛇添足”吗?

首先我们来总结一下左值引用的好处

  • 做函数参数:能够减少拷贝,提高效率,可以作为输出型参数
  • 做返回值:能够减少拷贝,提高效率

虽然左值引用有以上优点,但是,如果遇到下面的状况:

//状况一
template<class T>
T func(const T& val)
{T ret;//...return ret;
}
//状况二
void Test5()
{zht::string str;str = zht::to_string(-1234);
}

此时ret和str出了作用域之后就会销毁,如果T是类似string的对象,就需要进行深拷贝,所以就会造成效率降低。

这里我们采用自己实现的string,能够更清晰的看到调用情况:在这个string中增加了一些输出信息

namespace  zht
{
class string
{
public:typedef char* iterator;iterator begin(){return _str;}iterator end(){return _str + _size;}string(const char* str = ""):_size(strlen(str)), _capacity(_size){cout << "string(const char* str = "") -- 构造函数" << endl;_str = new char[_capacity + 1];strcpy(_str, str);}// s1.swap(s2)void swap(string& s){std::swap(_str, s._str);std::swap(_size, s._size);std::swap(_capacity, s._capacity);}// 拷贝构造string(const string& s):_str(nullptr){cout << "string(const string& s) -- 深拷贝" << endl;_str = new char[s._capacity + 1];strcpy(_str, s._str);_size = s._size;_capacity = s._capacity;}// 赋值重载string& operator=(const string& s){cout << "string& operator=(string s) -- 深拷贝 " << endl;if (this != &s){delete[] _str;_str = new char[s._capacity + 1];strcpy(_str, s._str);_size = s._size;_capacity = s._capacity;}return *this;}~string(){delete[] _str;_str = nullptr;}char& operator[](size_t pos){assert(pos < _size);return _str[pos];}void reserve(size_t n){if (n > _capacity){char* tmp = new char[n + 1];strcpy(tmp, _str);delete[] _str;_str = tmp;_capacity = n;}}void push_back(char ch){if (_size >= _capacity){size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;reserve(newcapacity);}_str[_size] = ch;++_size;_str[_size] = '\0';}string& operator+=(char ch){push_back(ch);return *this;}const char* c_str() const{return _str;}
private:char* _str;size_t _size;size_t _capacity; // 不包含最后做标识的\0
};
string to_string(int value)
{bool flag = true;if (value < 0){flag = false;value = 0 - value;}string str;while (value > 0){int x = value % 10;value /= 10;str += ('0' + x);}if (flag == false){str += '-';}std::reverse(str.begin(), str.end());return str;
}
}

运行上述情况二的代码可以看到:在过程中进行了深拷贝,这里深拷贝的代价就非常的大,也就是左值引用的短板所在,因此提出了右值引用的概念。

image-20230719173255829

使用右值引用和移动语义解决上述问题:

在此之前,我们明确一个概念:在C++11中,对右值引用进行了一个分类,将其分为纯右值将亡值两种,其中纯右值指的是内置类型表达式的值,将亡值是指自定义类型的表达式的值,所谓的将亡值也就是指生命周期将要结束的值,一般来说,匿名对象、临时对象、move后的自定义类型对象都是将亡值。

让我们思考一下,在上述情景过程中,进行深拷贝之后,原来的局部变量str是会被析构掉的,相当于我们先构造一个一摸一样的变量,然后再析构掉这个局部变量,那么我们不如直接将这个变量的资源交给另一个变量管理,这样就能够提高效率。

于是就有了移动构造这个接口:移动构造也是一个构造函数,它的参数是类型的右值引用,实际上就是把传入右值的资源转移过来,避免了深拷贝,所以称为移动构造,就是移动别人的资源来进行构造。

接下来我们来实现一下上面string的移动构造和移动赋值

// 移动构造
string(string&& s):_str(nullptr), _size(0), _capacity(0)
{cout << "string(string&& s) -- 移动构造" << endl;swap(s);
}
// 移动赋值
string& operator=(string&& s)
{cout << "string& operator=(string&& s) -- 移动赋值" << endl;swap(s);return *this;
}

在我们自己实现的string中加入了这两个函数之后,再次运行刚刚的程序,就会发现函数调用改变了:深拷贝变成了移动拷贝

image-20230719171811137

下面,我们来分析一下为什么会变成这样:

加入移动语义之前:

image-20230719174042866

加入移动语义之后:

image-20230719174355620

注:有种说法是右值引用延长了变量的生命周期,事实上这种说法是不准确的,他只是将一个变量的资源转移给了另一个变量,此变量本身的生命周期是没有变化的。如果一定要这样说的话,那可以理解成延长了这个资源的生命周期(但是资源没有生命周期这一说)。

在C++11之后,STL容器中都增加了移动构造和移动复制的接口

image-20230720004207405

image-20230720004237237

1.4 左值引用和右值引用的深入使用场景分析

根据上文,我们知道右值只能引用右值,但是右值一定不能引用左值吗?

在有些场景下,我们可能真的需要右值去引用左值,从而实现移动语义。

当需要右值引用一个左值的时候,可以通过move函数将左值转变为右值。在C++11中,std::move()函数在<utility>头文件中,这个函数只有一个唯一的作用就是将左值强制转换成右值

image-20230720004423230

可以看到使用move将s1从左值变成右值之后,再次插入编译器就将s1识别成将亡值,匹配到移动构造然后未查到lt中。

根据上述的例子我们知道库里面的list是支持移动构造的,我们之前也模拟实现过list,那么现在能否对之前的list进行一个改造,让其也能支持移动语义呢?

首先在这里附上之前实现的list的源码

namespace zht
{template<class T>struct __list_node{__list_node* _prev;__list_node* _next;T _data;__list_node(const T& data = T()):_data(data), _prev(nullptr), _next(nullptr){}};template<class T, class Ref, class Ptr>struct __list_iterator{typedef __list_node<T> node;typedef __list_iterator<T, Ref, Ptr> Self;node* _pnode;__list_iterator(node* p):_pnode(p){}Ptr operator->(){return &operator*();}Ref operator*(){return _pnode->_data;}Self& operator++(){_pnode = _pnode->_next;return *this;}Self operator++(int){Self tmp(*this);_pnode = _pnode->_next;return tmp;}Self& operator--(){_pnode = _pnode->_prev;return *this;}Self operator--(int){Self tmp(*this);_pnode = _pnode->_prev;return tmp;}bool operator!=(const Self& it){return _pnode != it._pnode;}bool operator==(const Self& it){return _pnode == it._pnode;}};template<class T>class list{typedef __list_node<T> node;public:typedef __list_iterator<T, T&, T*> iterator;typedef __list_iterator<T, const T&, const T*> const_iterator;iterator begin(){return _head->_next;}iterator end(){return _head;}const_iterator begin() const{return _head->_next;}const_iterator end() const{return _head;}void empty_initialize(){_head = new node(T());_head->_next = _head;_head->_prev = _head;_size = 0;}list(){empty_initialize();}list(size_t n, const T& val = T()){empty_initialize();for (size_t i = 0; i < n; ++i){push_back(val);}}template<class InputIterator>list(InputIterator first, InputIterator last){empty_initialize();while (first != last){push_back(*first);++first;}}list(const list<T>& lt)//经典写法{empty_initialize();for (const auto& e : lt){push_back(e);}}void swap(list<T>& lt){std::swap(_head, lt._head);}list<T>& operator=(list<T>& lt){if (this != *lt){clear();for (auto& e : lt){push_back(e);}}}bool empty(){return _head->_next == _head;}void clear(){while (!empty()){erase(--end());}}~list(){clear();delete _head;_head = nullptr;}void push_back(const T& val = T()){insert(end(), val);}void push_front(const T& val = T()){insert(begin(), val);}iterator insert(iterator pos, const T& val = T()){node* newnode = new node(val);node* prev = pos._pnode->_prev;node* cur = pos._pnode;prev->_next = newnode;newnode->_prev = prev;newnode->_next = cur;cur->_prev = newnode;++_size;return iterator(newnode);}size_t size(){return _size;}iterator erase(iterator pos){assert(pos != end());node* prev = pos._pnode->_prev;node* next = pos._pnode->_next;prev->_next = next;next->_prev = prev;delete pos._pnode;--_size;return iterator(next);}void pop_back(){erase(--end());}void pop_front(){erase(begin());}void resize(size_t n, const T& val = T()){while (n < size()){pop_back();}while (n > size()){push_back(val);}}private:node* _head;size_t _size;};
}

使用我们自己实现的list执行1.4中的Test6,将会得到如下结果:

image-20230720011329456

为了让我们的list也能像库里面的list一样,我们首先考虑到的就是实现push_back的右值引用版本,由于在push_back中调用了insert,所以insert也需要增加右值引用的版本,同样的在node的构造中也需要增加右值引用版本的构造函数,所以增加的函数如下:

//template<class T> struct __list_node
__list_node(T&& data):_data(data), _prev(nullptr), _next(nullptr)
{}
//template<class T> class list
void push_back(T&& val)
{insert(end(), val);
}
iterator insert(iterator pos, T&& val)
{node* newnode = new node(val);node* prev = pos._pnode->_prev;node* cur = pos._pnode;prev->_next = newnode;newnode->_prev = prev;newnode->_next = cur;cur->_prev = newnode;++_size;return iterator(newnode);
}

那么现在再来尝试执行以下,发现没有变化,这是为什么呢?

(这里大家可以自己去实践一下)通过调试可以发现确实调用了右值引用版本的push_back,但是继续往下面调试就发现调用的是左值版本的insert,这是什么原因呢??

因为右值引用之后变量本身是左值,所以这里val的属性就是一个左值,所以当然会匹配到左值版本的insert,所以这里在传参的时候需要将这个val的属性变成右值,这里可以使用move来改一下传参之后的属性,然后再编译运行,发现还是和原来的结果一样,这是因为函数套函数,每一层都需要将参数属性改为右值,非常的麻烦,如果我们将所有的参数都用move修改一下之后:

__list_node(T&& data):_data(move(data)), _prev(nullptr), _next(nullptr)
{}		
void push_back(T&& val)
{insert(end(), move(val));
}
iterator insert(iterator pos, T&& val)
{node* newnode = new node(move(val));node* prev = pos._pnode->_prev;node* cur = pos._pnode;prev->_next = newnode;newnode->_prev = prev;newnode->_next = cur;cur->_prev = newnode;++_size;return iterator(newnode);
}

再次运行之前的代码,可以看到已经实现了库里面的效果,这里多了一个string的构造和移动构造是因为初始化lt的时候调用的,由于我们string和list的实现和库里面还是有些许不同点的。

image-20230720144831650

至此,我们就完成了自己的list中对移动语义的支持了。

1.5 完美转发

1.5.1 万能引用

我们上面都是单独定义一个参数为右值引用的函数,然后让编译器根据实参的类型来选择调用参数为左值引用的构造/插入接口还是参数为右值引用的构造/插入接口。那么,我们能不能让函数能够根据实参的类型自动实例化出对应不同的函数呢?

万能引用可以实现这个功能。

所谓的万能引用,实际上是一个模板,且函数的形参为右值引用。对于这种模板,编译器能够根据实参的类型自动推衍实例化出不同的形参类型,这个模板能够接收左值/const左值/右值/const右值,推衍出的类型为:左值引用/const左值引用/右值引用/const右值引用

举个例子

template<class T>
void PerfectForward(T&& x)
{cout << "void PerfectForward(int&& x)" << endl;
}
void Test7()
{PerfectForward(10); // 右值int a = 10;PerfectForward(a); //左值const int b = 8;PerfectForward(b); //const 左值PerfectForward(std::move(b)); //const右值
}

四个变量同时调用PerfectForward,都能够调,不会报错。

image-20230720154455772

image-20230720155149925

这里提一下,如果这里的四个调用全部都出现了,那么其中的x就不能改变

image-20230720155342785

❓但是如果将后面两个const调用的语句屏蔽掉就能够编译成功,这是什么原因呢?

✅我们写的PerfectForward是一个函数模板,在编译运行的过程中会实例化出来四个不同的函数,由于调用传参的过程中有const修饰的形参被实例化,所以实例化后的此函数就会报错。

1.5.2 完美转发

接下来我们将上述的代码进行一点点更改

void Func(int& x)
{cout << "lvalue reference" << endl;
}
void Func(int&& x)
{cout << "rvalue reference " << endl;
}
void Func(const int& x)
{cout << "const lvalue reference " << endl;
}
void Func(const int&& x)
{cout << "const rvalue reference " << endl;
}
template<class T>
void PerfectForward(T&& x)
{Func(x);
}
void Test7()
{PerfectForward(10); // 右值int a = 10;PerfectForward(a); //左值const int b = 8;PerfectForward(b); //const 左值PerfectForward(std::move(b)); //const右值
}

image-20230720160515000

运行这个代码我们发现,不管是左值引用还是右值引用,只能调用到左值版本的Func函数,原因在上文中已经说过了:右值引用之后变量本身是左值,所以只能调用到左值版本的Func,那么如何能让左值的调用左值版本,右值的调用右值版本呢?

使用完美转发std::forward,std::forward 完美转发在传参的过程中保留对象原生类型属性

image-20230720160812989

2. 新的类功能

2.1 默认成员函数

在之前的文章【C++】类和对象中我们讲到类的默认成员函数一共有六个:

  • 构造函数
  • 析构函数
  • 拷贝构造函数
  • 赋值重载函数
  • 取地址重载
  • const取地址重载

其中重要的是前4个,后两个用处不大,但是在C++11中新增了两个默认成员函数:移动构造函数和移动赋值重载函数,这就与我们上文中讲到的对应起来了。

同样的这两个函数也有一些比较难搞的特性:

1. 移动构造函数

编译器自动生成的条件:

  1. 没有自己实现移动构造;
  2. 没有实现析构函数、拷贝构造、拷贝赋值重载中的任意一个

自动生成的移动构造的特性:

  • 对于内置类型,将会逐成员按字节进行拷贝
  • 对于自定义类型,如果这个类型成员有移动构造就调用移动构造,否则就调用拷贝构造

2. 移动赋值重载函数

编译器自动生成的条件

  1. 没有自己实现移动赋值重载
  2. 没有实现析构函数、拷贝构造、拷贝赋值重载中的任意一个

自动生成的移动赋值重载的特性:

  • 对于内置类型,将会逐成员按字节进行拷贝
  • 对于自定义类型,如果这个类型成员有移动赋值就调用移动赋值,否则就调用移动赋值

我们看下面一段代码:

class Person
{
public:Person(const char* name = "", int age = 0):_name(name), _age(age){}
private:zht::string _name;int _age;
};
void Test8()
{Person p1;Person p2 = p1;Person p3 = std::move(p1);Person p4;p4 = std::move(p2);
}

运行结果如下:

image-20230721230445652

分析一下这个结果:

首先可以看到的是Person这个类没有实现移动构造,并且没有实现析构函数、拷贝构造、拷贝赋值重载中的任意一个,所以编译器自动生成了移动构造和其他的默认构造函数。

对于第15行内容,由于p1是左值,所以匹配到拷贝构造,Person中默认生成的拷贝构造会调用zht::string中的拷贝构造,因此输出string(const string& s) -- 深拷贝,对于第16行,将p1move之后,变成了右值,因此调用Person自动生成的移动构造,这个移动构造将会调用zht::string中的移动构造,因此输出string(string&& s) -- 移动构造,对于18行,p2move之后变成右值,调用Person自动生成的移动赋值,此函数调用zht::string中的移动赋值,因此输出string& operator=(string&& s) -- 移动赋值

2.2 类成员变量初始化

C++11允许类定义的时候给定成员变量的初始缺省值,默认生成的构造函数在初始化列表的时候将会使用这些缺省值初始化。

看下面一段代码:

class Date1
{
public:int _year;int _month;int _day;
};
class Date2
{
public:int _year = 1970;int _month = 1;int _day = 1;
};void Test9()
{Date1 d11;Date2 d12;
}

image-20230722014627072

在调试过程看到:Date1对应的对象d11中的成员变量都没有被初始化,还是随机值,但是Date2对应的对象d12被默认初始化成了缺省值。

2.3 强制生成默认函数的关键字defaule

在2.1中我们讲到移动构造和移动赋值的默认生成条件比较苛刻。假设我们已经写了拷贝构造,就不会生成移动构造了,但是我们希望移动构造能够被自动生成,就可以使用default关键字显示指定移动构造生成。

class Person
{
public:Person(const char* name = "", int age = 0):_name(name), _age(age){}Person(const Person& p):_name(p._name),_age(p._age){}//这里使用default显示指定移动构造和移动赋值生成Person(Person&& p) = default;Person& operator=(Person&& p) = default;Person& operator=(const Person& p){if(this != &p){_name = p._name;_age = p._age;}return *this;}~Person(){}
private:zht::string _name;int _age;
};
void Test8()
{Person p1;Person p2 = p1;Person p3 = std::move(p1);Person p4;p4 = std::move(p2);
}

image-20230722015921822

可以看见此时已经手动写了拷贝构造和拷贝赋值重载,但是还是生成了移动构造和移动赋值重载。

2.4 禁止生成默认函数的关键字delete

如果想要限制某些默认成员函数的生成或者使用,在C++98中的做法是:将该函数设置成private,并且只生成不定义,这样在类外调用的时候就会报错。

在C++11中的做法就更加简单了:只需要在该函数的声明中加上=delete即可,该语法指示编译器不生成对应函数的默认版本,称=delete修饰的函数为删除函数。

class Person
{
public:Person(const char* name = "", int age = 0):_name(name), _age(age){}Person(const Person& p) = delete;
private:zht::string _name;int _age;
};
void Test8()
{Person p1;Person p2 = p1;Person p3 = std::move(p1);Person p4;p4 = std::move(p2);
}

image-20230722021202591

此时调用拷贝构造就会出现报错:尝试引用已删除的函数

2.5继承和多态中的final与override关键字

关于final和override关键字,在之前的博客中已经讲解,这里就不在赘述,有需要的小伙伴可以去看一下

【C++】多态

3. 可变参数模版

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

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

3.1 可变参数模板的语法

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

image-20230722023240796

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

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

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

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

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

image-20230722025553608

这里有一个点需要注意:我们需要将代表参数包的...放在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,剩下的参数都放进从参数包中,递归调用。

image-20230722032322286

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。

image-20230722033955948

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

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

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

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

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

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

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

4. lambda表达式

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

image-20230722035630950

可以看到,如果想要排序内置类型,可以使用仿函数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;
}

image-20230725210913074

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表达式,没有任何实际意义[] {};// 省略参数列表和返回值类型,返回值类型由编译器推导为intint 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;// 赋值捕捉xint 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函数的语句块

  2. 语法上捕捉列表可以由多个捕捉项组成,并以逗号分隔

    例如:[=, &a, &b]:表示以引用传递的放啊是捕捉a和b,值传递方式捕捉其他所有变量

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

  3. 捕捉列表不允许变量重复传递,否则就会导致编译错误

    在第2点上我们见到了[=, &a]这种用法,默认按照值传递的方式捕捉了所有的变量,但是把a单独拿出来说,意思将a单独处理,按照引用的方式传递。但是如果这里换成[=, a],就出现了重复传递,会导致编译错误。

  4. 在块作用域以外的lambda函数捕捉列表必须为空

  5. 在块作用域中的lambda函数仅能捕捉父作用域中局部变量,捕捉任何非此作用域或者非局部变量都会导致编译报错

  6. lambda表达式之间不能相互赋值,及时看起来类型相同

image-20230726182710495

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都可以像函数一样使用。

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

image-20230726205554268

我们首先看汇编语言中的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表达式底层类名都是唯一的。


本节完 …

相关文章:

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

文章目录 1. 右值引用和移动语义1.1 左值引用和右值引用1.2 左值引用和右值引用的比较1.3右值引用的使用场景和意义1.4 左值引用和右值引用的深入使用场景分析1.5 完美转发1.5.1 万能引用1.5.2 完美转发 2. 新的类功能2.1 默认成员函数2.2 类成员变量初始化2.3 强制生成默认函数…...

rust学习-线程

Rust 标准库只提供了 1:1 线程模型 Rust 是较为底层的语言&#xff0c;如果愿意牺牲性能来换取抽象&#xff0c;以获得对线程运行更精细的控制及更低的上下文切换成本&#xff0c;使用实现了 M:N 线程模型的 crate 示例 use std::thread; use std::time::Duration;fn main() …...

题目:2180.统计各位数字之和为偶数的整数个数

​​题目来源&#xff1a; leetcode题目&#xff0c;网址&#xff1a;2180. 统计各位数字之和为偶数的整数个数 - 力扣&#xff08;LeetCode&#xff09; 解题思路&#xff1a; 暴力遍历即可。 解题代码&#xff1a; class Solution {public int countEven(int num) {int re…...

3dsmax制作一个机器人

文章目录 建模身子&#xff1a;眼睛&#xff1a;头饰&#xff1a;肩膀手臂腿调整细节 渲染导出objMarmoset Toolbag 3.08渲染给眼睛添加材质&#xff0c;设置为自发光添加背景灯光 建模 身子&#xff1a; 眼睛&#xff1a; 头饰&#xff1a; 肩膀 手臂 腿 调整细节 渲染 导出…...

C++的类型转换运算符:reinterpret_cast

C的类型转换运算符&#xff1a;reinterpret_cast reinterpret_cast 是 C 中与 C 风格类型转换最接近的类型转换运算符。它让程序员能够将一种对象类型转换为另一种&#xff0c;不管它们是否相关&#xff1b;也就是说&#xff0c;它使用如下所示的语法强制重新解释类型&#xf…...

flask中的cookies介绍

flask中的cookies介绍 “Cookie” 在 web 开发中是一种非常重要的技术&#xff0c;用于在客户端&#xff08;即用户的浏览器&#xff09;存储信息&#xff0c;以便在多个页面和多个访问会话之间保持状态。Cookies 通常用于记住用户的登录信息&#xff0c;跟踪用户在站点上的浏…...

adnroid 11. 0 Activity启动流程图解

从Launcher到ActivityTaskManager 从ActivityTaskManagerService 到 ApplicationThread 从ApplicationThread到onCreate...

了解Unity编辑器之组件篇Physics(四)

Physics&#xff1a;用于处理物理仿真和碰撞检测。它提供了一组功能强大的工具和算法&#xff0c;用于模拟真实世界中的物理行为&#xff0c;使游戏或应用程序更加真实和可信。 主要用途包括&#xff1a; 碰撞检测&#xff1a;Unity Physics 提供了高效的碰撞检测算法&#x…...

“数字中华 点亮未来”中华线上客户节 盛大开幕

2023年是中华保险数字化转型落地之年&#xff0c;峥嵘37载&#xff0c;中华保险在数字化转型上已经涌现了一批彰显辨识度、具有影响力的应用成果。7月15日&#xff0c;中华保险围绕数字化转型之路开展以“数字中华 点亮未来”为主题的37周年线上客户节活动&#xff0c;倾力打造…...

中文分词入门:使用IK分词器进行文本分词(附Java代码示例)

1. 介绍 中文分词是将连续的中文文本切分成一个个独立的词语的过程&#xff0c;是中文文本处理的基础。IK分词器是一个高效准确的中文分词工具&#xff0c;采用了"正向最大匹配"算法&#xff0c;并提供了丰富的功能和可定制选项。 2. IK分词器的特点 细粒度和颗粒…...

CTFSHOW web 信息收集

web入门的刷题 web1 教我们多看看源代码 web2 抓包 web3 抓包 web4 robots.txt robots.txt web5 phps源代码泄露 phps 就是php的源代码 用户无法访问 php 只可以通过phps来访问 web6 源代码备份 web7 git web8 svn web9 swp /index.php.swp web10 cookie web11 查域名…...

速锐得开发社区-新一代汽车网络通信技术CAN FD的特点归纳

随着汽车工业的快速发展&#xff0c;汽车逐渐走向智能化&#xff0c;功能也越来越丰富&#xff0c;例如特斯拉、比亚迪、理想汽车为代表&#xff0c;在车载导航、驻车雷达、胎压监测、倒车影像、无钥匙启动、定速巡航、自动泊车、高级辅助驾驶系统、自动驾驶、域控制器、智能网…...

Android adb shell 查看App内存(java堆内存/vss虚拟内存/详细的内存状况/内存快照hprof)和系统可用内存

1.adb shell 获取app 进程的pid adb shell "ps|grep com.xxx包名"根据某个渠道包&#xff0c;去查询对应的pid&#xff0c;如下所示&#xff1a; 2.通过adb shell 查看设备的java dalvik 堆内存的最大值 执行命令行&#xff1a; adb shell getprop dalvik.vm.h…...

java篇 类的进阶0x0A:万类之祖:Object 类

文章目录 万类之祖&#xff1a;Object 类hashCode() 与 equals()hashCode() 方法equals() 方法 vs. equals()String 的 equals() 为什么要重写 hashCode 和 equals 方法重写&#xff08;覆盖&#xff09;前 hashCode() 和 equals() 的作用什么情况下需要重写&#xff08;覆盖&a…...

AVFoundation - 音频录制

文章目录 需要调用到麦克风方法,别忘记添加 Privacy - Microphone Usage Description @interface AudioRecorder ()<AVAudioRecorderDelegate>@property (strong, nonatomic) AVAudioRecorder *recorder;@end@implementation AudioRecorder- (void...

Jmeter+MySQL链接+JDBC Connection配置元件+使用

参考大大的博客学习&#xff1a;怎么用JMeter操作MySQL数据库&#xff1f;看完秒懂&#xff01;_jmeter mysql_程序员馨馨的博客-CSDN博客 注&#xff1a;里面所有没打码的都是假数据&#xff0c;麻烦大家自行修改正确的信息。 一、背景 需要取数据库中的值&#xff0c;作为…...

统一观测丨使用 Prometheus 监控 Cassandra 数据库最佳实践

作者&#xff1a;元格 本篇内容主要包括四部分&#xff1a;Cassandra 概览介绍、常见关键指标解读、常见告警规则解读、如何通过 Prometheus 建立相应监控体系。 Cassandra 简介 Cassandra 是什么&#xff1f; Apache Cassandra 是一个开源、分布式、去中心化、弹性可伸缩、…...

Hive视图

hive的视图 简介 hive的视图简单理解为逻辑上的表hive只支持逻辑视图&#xff0c;不支持物化视图视图存在的意义 对数据进行局部暴露&#xff08;涉及隐私的数据不暴露&#xff09;简化复杂查询 创建视图&#xff1a; create view if not exists v_1 as select uid,movie f…...

node中使用jsonwebtoken实现身份认证

在现代web应用中&#xff0c;用户身份认证是非常重要且必不可少的一环。而使用Node.js和Express框架&#xff0c;可以方便地实现用户身份认证。而在这个过程中&#xff0c;jsonwebtoken这个基于JWT协议的模块可以帮助我们实现安全且可靠的身份认证机制&#xff0c;可以让我们轻…...

pyspark笔记:读取 处理csv文件

pyspark cmd上的命令 1 读取文件 1.1 基本读取方式 注意读取出来的格式是Pyspark DataFrame&#xff0c;不是DataFrame&#xff0c;所以一些操作上是有区别的 1.1.1 format DataFrame spark.read.format("csv").option(name,value).load(path) format表示读取…...

【大模型RAG】拍照搜题技术架构速览:三层管道、两级检索、兜底大模型

摘要 拍照搜题系统采用“三层管道&#xff08;多模态 OCR → 语义检索 → 答案渲染&#xff09;、两级检索&#xff08;倒排 BM25 向量 HNSW&#xff09;并以大语言模型兜底”的整体框架&#xff1a; 多模态 OCR 层 将题目图片经过超分、去噪、倾斜校正后&#xff0c;分别用…...

日语AI面试高效通关秘籍:专业解读与青柚面试智能助攻

在如今就业市场竞争日益激烈的背景下&#xff0c;越来越多的求职者将目光投向了日本及中日双语岗位。但是&#xff0c;一场日语面试往往让许多人感到步履维艰。你是否也曾因为面试官抛出的“刁钻问题”而心生畏惧&#xff1f;面对生疏的日语交流环境&#xff0c;即便提前恶补了…...

云原生核心技术 (7/12): K8s 核心概念白话解读(上):Pod 和 Deployment 究竟是什么?

大家好&#xff0c;欢迎来到《云原生核心技术》系列的第七篇&#xff01; 在上一篇&#xff0c;我们成功地使用 Minikube 或 kind 在自己的电脑上搭建起了一个迷你但功能完备的 Kubernetes 集群。现在&#xff0c;我们就像一个拥有了一块崭新数字土地的农场主&#xff0c;是时…...

Java 语言特性(面试系列1)

一、面向对象编程 1. 封装&#xff08;Encapsulation&#xff09; 定义&#xff1a;将数据&#xff08;属性&#xff09;和操作数据的方法绑定在一起&#xff0c;通过访问控制符&#xff08;private、protected、public&#xff09;隐藏内部实现细节。示例&#xff1a; public …...

反向工程与模型迁移:打造未来商品详情API的可持续创新体系

在电商行业蓬勃发展的当下&#xff0c;商品详情API作为连接电商平台与开发者、商家及用户的关键纽带&#xff0c;其重要性日益凸显。传统商品详情API主要聚焦于商品基本信息&#xff08;如名称、价格、库存等&#xff09;的获取与展示&#xff0c;已难以满足市场对个性化、智能…...

FastAPI 教程:从入门到实践

FastAPI 是一个现代、快速&#xff08;高性能&#xff09;的 Web 框架&#xff0c;用于构建 API&#xff0c;支持 Python 3.6。它基于标准 Python 类型提示&#xff0c;易于学习且功能强大。以下是一个完整的 FastAPI 入门教程&#xff0c;涵盖从环境搭建到创建并运行一个简单的…...

屋顶变身“发电站” ,中天合创屋面分布式光伏发电项目顺利并网!

5月28日&#xff0c;中天合创屋面分布式光伏发电项目顺利并网发电&#xff0c;该项目位于内蒙古自治区鄂尔多斯市乌审旗&#xff0c;项目利用中天合创聚乙烯、聚丙烯仓库屋面作为场地建设光伏电站&#xff0c;总装机容量为9.96MWp。 项目投运后&#xff0c;每年可节约标煤3670…...

DIY|Mac 搭建 ESP-IDF 开发环境及编译小智 AI

前一阵子在百度 AI 开发者大会上&#xff0c;看到基于小智 AI DIY 玩具的演示&#xff0c;感觉有点意思&#xff0c;想着自己也来试试。 如果只是想烧录现成的固件&#xff0c;乐鑫官方除了提供了 Windows 版本的 Flash 下载工具 之外&#xff0c;还提供了基于网页版的 ESP LA…...

python爬虫:Newspaper3k 的详细使用(好用的新闻网站文章抓取和解析的Python库)

更多内容请见: 爬虫和逆向教程-专栏介绍和目录 文章目录 一、Newspaper3k 概述1.1 Newspaper3k 介绍1.2 主要功能1.3 典型应用场景1.4 安装二、基本用法2.2 提取单篇文章的内容2.2 处理多篇文档三、高级选项3.1 自定义配置3.2 分析文章情感四、实战案例4.1 构建新闻摘要聚合器…...

【配置 YOLOX 用于按目录分类的图片数据集】

现在的图标点选越来越多&#xff0c;如何一步解决&#xff0c;采用 YOLOX 目标检测模式则可以轻松解决 要在 YOLOX 中使用按目录分类的图片数据集&#xff08;每个目录代表一个类别&#xff0c;目录下是该类别的所有图片&#xff09;&#xff0c;你需要进行以下配置步骤&#x…...