C++——C++11常用语法总结
C++11标准由国际标准化组织(ISO)和国际电工委员会(IEC)旗下的C++标准委员会(ISO/IEC JTC1/SC22/WG21)于2011年8月12日公布,并于2011年9月出版。2012年2月28日的国际标准草案(N3376)是最接近于C++11标准的草案(仅编辑上的修正)。此次标准为C++98发布后13年来第一次重大修正。
1. 列表初始化
在C++98中,数组、结构体可以用{}进行初始化这是我们早就习以为常的事情。
struct A {int a;char b;};void Test1(){A a = { 1,'g' };int arr1[] = { 1,2,3,4 };int arr2[10] = { 0 };}
在C++11的标准中,扩大{}形成的列表的适用范围,可用于所有内置类型和自定义类型。具体体现在:new表达式也可以使用列表初始化。可以使用列表初始化的方式调用构造函数,当使用列表来初始化时等号可有可无。
class Date {public:Date(int year, int month, int day):_year(year), _month(month), _day(day){}private:int _year;int _month;int _day;};void Test2(){int n1 = 1;int n2 = { 2 };int n3{ 3 };A a{ 1,'g' };Date d{ 2004,3,25 };//隐式类型转换,列表和构造函数参数必须一一对应int arr[]{ 1,2,3,4 };int* p = new int[3] {1, 2, 3};}
在std标准库中有一个初始化列表类:initializer_list,将其作为构造函数的参数,那么初始化就很方便了。如下文的代码,{1, 2, 3, 4} 是一个std::initializer_list<int>类型的对象,编译器查找到了std::vector<int>中有接受std::initializer_list<int>的构造函数:vector(std::initializer_list<int>); 所以编译器调用这个构造函数来创建v。
void Test3(){vector<int> v = { 1,2,3,4 };//初始化列表类的对象,其中的值可以任意多个}
2. 声明的特殊方法
2.1 auto类型
在C++98中,auto是变量是局部自动存储类型,但由于局部域中定义局部的变量默认就是自动存储类型,所以不会刻意被使用。而在C++11中,auto废弃了原来的用法,变成自动推断变量声明类型。当使用auto时,必须进行显式初始化以便编译器推导类型。
void Test1(){int x = 1;auto p = &x;cout << typeid(p).name() << endl;}
2.2 decltype
decltype可以根据所给表达式推导声明类型。
void Test2(){int x = 4;double y = 10.2;decltype(x * y) ret;cout << typeid(ret).name() << endl;}
2.3 nullptr
专指空指针。
3. 范围for循环
实际上是基于迭代器的for循环,以前的文章介绍过,不再解释。
4. 智能指针
智能指针篇幅较大,将在不久之后的文章中重点讲解。
5. 新容器
C++11引入新容器:array(静态数组),forward_list(单链表),unordered_map/set(哈希map、set),都已经介绍过。
6. 右值引用
我们以前就接触过引用这种类型,它实际上是给变量起了个别名。在C++11中引入了右值引用的概念,于是引用就分为了左值引用和右值引用。
6.1 左值和右值
左值——可以通俗简单的理解为可以取地址的值,如变量、指针等都是左值。之所以叫作左值是因为其多位于等号左边,可以被赋值。
//以下如a、p、s、b、*p都是左值//左值多位于等号左边,即可以被赋值int a = 10;char* p = new char('a');string s("hello world");const int b = 9;*p = 'c';
右值——可以理解为不可以取地址的值,如临时变量、常量、匿名对象等。同样的,右值多位于等号右侧,可以为左值赋值,但其自身不可以被赋值。这并不难理解,因为右值之所以不允许取地址正是因为它具有“临时”这个属性,转瞬即逝,赋值没有意义。这也就解释了为什么以前说临时变量具有常性。
右值例如表达式的结果、常数、函数返回值等,它们并非在计算机中不存在,而是在程序运行中可能仅仅在当前语句会存在于寄存器中或内存中,属于一个过程量,没有单独拿出来赋值的必要。在右值引用引用右值后,右值会存储到特定位置并且支持通过右值引用访问。
//如下表达式为右值,包括常量、常量临时对象、匿名对象//右值多位于等号右边,即可以给变量赋值,但自身不能赋值(临时的对象,由临时空间存储)20;a + b;fmin(a, b);string("good");
6.2 左值引用和右值引用
6.2.1 左值引用和右值引用的使用
左值引用就是给左值取别名,我们过去使用的那种最常见的引用都是左值引用。
//左值引用:给左值起别名int& r1 = a;char*& r2 = p;string& r3 = s;const int& r4 = b;char& r5 = *p;
右值引用则是给右值取别名。
//右值引用:给右值起别名int&& rr1 = 20;int&& rr2 = a + b;int&& rr3 = fmin(a , b);string&& rr4 = string("hello");
6.2.2 左值引用 引用 右值 与 右值引用 引用 左值
①左值引用引用右值时不可以直接引用,需要const左值引用给右值起别名。
//左值引用引用右值:不可以直接引用,需要const左值引用给右值起别名const int& rx1 = 20;const int& rx2 = a + b;const int& rx3 = fmin(a, b);const string& rx4 = string("hello");
根据这一特性,我们的函数传参可以用const左值引用接收右值,而以往我们确实也是这么干的,通过const T&接收常量等右值。
②右值引用引用左值时不可以直接引用,需要使用move()将其转化为右值。
int&& rxx1 = move(a);char*&& rxx2 = move(p);string&& rxx3 = move(s);const int&& rxx4 = move(b);char&& rxx5 = move(*p);int&& rxx6 = (int&&)a;
注:如果深入汇编层查看,会发现汇编中根本没有左值引用、右值引用,而且甚至没有引用的概念。实际上左值引用和右值引用都是指针的变形,底层都是通过指针实现的,在高级语言层包装成为所谓的引用则是为了方便使用。因此,move()本质上是应付语法检查,并没有对变量本身做任何修改,可以理解为强制类型转换为右值。
于是我们发现,右值作为实参,优先匹配右值引用的形参,之后才是const左值引用。
6.3 移动构造和移动复制
6.3.1 移动构造的原理
// 移动构造——将参数对象的内容直接移动到自己的名下,作为自己的内容// 深拷贝的类,移动构造才有意义// 当参数为右值时优先调用移动构造,若没有移动构造则调用拷贝构造string(string&& s){swap(s);}
移动构造,就是参数接收一个右值的对象,然后将this和接收到的对象进行swap交换资源。
会发现,这本质上就是一种“掠夺”的行径,强制传入的参数和自己的资源交换。然而正是这种方式最适应于传入参数是没有用却有资源的对象,而自己是有用却没有资源的对象。而在现实中恰恰存在这种场景,存在一些有资源的将亡值,这些对象就可以被用来作为移动构造的牺牲品(参数)。
对于深拷贝来说,这种构造方法规避了开销巨大的拷贝动作,而是简单的交换资源便完成了对象的构造,显然极大提高了深拷贝的类的对象构造的效率。对于浅拷贝而言,自然就和一般的构造一样没什么优势了。
6.3.2 移动构造举例
我们曾经引入引用,解决了参数传递时开销大的问题,但是并没有完全解决传递返回值的问题,当返回值是局部对象时就会产生拷贝构造降低效率,见如下例子。
对于一个string类,它有构造函数:
无参构造
string(const char* str = ""):_size(strlen(str)), _capacity(_size){//cout << "string(char* str)" << endl;_str = new char[_capacity + 1];strcpy(_str, str);}
拷贝构造
// 拷贝构造——将参数对象的内容拷贝后形成的备份作为自己的内容// s2(s1)string(const string& s):_str(nullptr){cout << "string(const string& s) -- 深拷贝" << endl;reserve(s._capacity);for (auto ch : s){push_back(ch);}}
有一个函数需要返回string对象。
//对于返回值是对象的函数,如String::string i2s(int value){bool flag = true;if (value < 0){flag = false;value = 0 - value;}String::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;}
使用如下代码调用函数,由于拷贝构造需要深拷贝开销最大,所以关注拷贝构造次数便可以见微知著。
String::string ret1 = i2s(1234);
无移动构造、无优化时编译器操作:
①进入i2s函数,调用无参构造函数构造str;
②完成函数体后返回局部变量str,函数结束栈帧销毁,str也会销毁,所以str通过拷贝构造函数构造一个临时变量;
③临时变量将对象交给ret1,于是调用拷贝构造函数,通过临时变量构造ret1。
通过以上步骤,发现此时调用了两次拷贝构造。
无移动构造、有优化时的编译器操作:
①进入i2s函数,调用无参构造函数构造str;
②完成函数体后返回局部变量str,str直接通过拷贝构造函数构造ret1。
可以发现,优化后编译器略去了中间的临时变量,只调用了一次拷贝构造。
移动构造、有优化时的编译器操作:
string(string&& s){cout << "string(string&& s) -- 移动拷贝" << endl;swap(s);}
①进入i2s函数,调用无参构造函数构造str;
②完成函数体后返回局部变量str,str通过移动构造函数构造ret1。
③构造完成后str变成空对象,随着栈帧一起消逝,而et1获得了str的资源。
可以看到,用将亡值str构造ret1没有任何影响,而且仅调用移动构造。
移动构造、大力度优化编译器操作:
①编译器直接采用一次默认构造,用ret1替代函数中的str直接构造。
此时仅使用了一次无参构造。
总结:
可以看到移动构造对于深拷贝的优化是很大很有用的。需要一提的是,想让函数返回值调用移动构造时,返回值可以不用显式调用move,有移动拷贝的时候会隐式类型转换。
//return move(str);//可以不用显式调用move,有移动拷贝的时候会隐式类型转换return str;
std::list<String::string> l1;String::string s1("abcd");//list支持右值引用传参// void push_back (const value_type& val);// void push_back (value_type&& val);l1.push_back(s1); //拷贝构造l1.push_back(String::string("aabb")); //移动构造l1.push_back("aaab"); //移动构造l1.push_back(move(s1)); //移动构造,但会导致s1被清空
6.3.3 移动赋值
移动赋值和移动构造原理一样,无需多解释。
// 移动赋值string& operator=(string&& s){swap(s);return *this;}
6.3.4 调用移动构造的要点
想调用移动构造并不难,因为移动构造的参数比拷贝构造更优先接收右值,所以确保传递的是右值即可。然而在实际代码中,这并不那么容易搞定。对于如下自己完成的string和list类为例。
函数调用
m_list::list<String::string> l1;String::string s1("abcd");l1.push_back(move(s1)); //移动构造
list::push_back
void push_back(const T& val){insert(end(), val);}//push_back的右值引用版本void push_back(T&& val){insert(end(), move(val));//val是左值属性,所以传递给insert时使用move,调用其右值版本}
list::insert
//插入void insert(iterator pos, const T& val){Node* next = pos._node;Node* prev = next->_prev;Node* newnode = new Node(val);prev->_next = newnode;newnode->_prev = prev;newnode->_next = next;next->_prev = newnode;++_size;}//右值版本void insert(iterator pos, T&& val){Node* next = pos._node;Node* prev = next->_prev;Node* newnode = new Node(move(val));//val是左值属性,所以构造Node时使用move,调用其右值版本prev->_next = newnode;newnode->_prev = prev;newnode->_next = next;next->_prev = newnode;++_size;}
ListNode(Node)构造函数
//构造函数,使用初始化列表初始化ListNode(const T& val = T()) //参数注意使用const引用,因为实参可能是常性的:_prev(nullptr), _next(nullptr), _val(val){}ListNode(T&& val) :_prev(nullptr), _next(nullptr), _val(move(val)) //val是左值属性,所以赋值时使用move,调用其右值版本{}
string构造函数
//无参构造string(const char* str = ""):_size(strlen(str)), _capacity(_size){//cout << "string(char* str)" << endl;_str = new char[_capacity + 1];strcpy(_str, str);}// 拷贝构造string(const string& s):_str(nullptr){reserve(s._capacity);for (auto ch : s){push_back(ch);}}// 移动构造string(string&& s){swap(s);}
移动构造路径:
①由l1.push_back(右值),调用push_back(T&& val)
②由push_back的insert(end(), move(val)),调用insert(iterator pos, T&& val)
③由insert的new Node(move(val)),调用ListNode(T&& val)
④由ListNode移动构造的_val(move(val)),调用string(string&& s)
以上步骤如若有一处没有使用move类型转换成右值就会导致调用拷贝构造。
6.4 完美转发
6.4.1 引例
考虑如下代码:
void Fun(int& x) { cout << "左值引用" << endl; }void Fun(const int& x) { cout << "const 左值引用" << endl; }void Fun(int&& x) { cout << "右值引用" << endl; }void Fun(const int&& x) { cout << "const 右值引用" << endl; }void PerfectForward(T&& t){Fun(t);cout << endl;}
首先需要说明的是,模板中的&&不代表右值引用,而是万能引用(引用折叠),表示其既能接收左值又能接收右值。这仅仅只是模板的&&才是万能引用,对于函数的参数&是左值引用,&&是右值引用。
void Test5(){PerfectForward(10); //右值int a;PerfectForward(a); //左值PerfectForward(std::move(a)); //右值const int b = 8;PerfectForward(b); //const 左值PerfectForward(std::move(b)); //const 右值}
会发现无论是左值或右值,经过PerfectForward函数后,统统调用了左值相关的函数。这是因为在传参中,无论是左值右值都会被形参t接收。作为左值引用的t是一个左值,而作为右值引用的t也退化为了左值。所以调用Fun的时候都调用了接收左值的函数。
6.4.2 完美转发原理与使用
面对上述的右值引用的属性是左值而导致的属性退化问题,可以使用标准库提供的完美转发来解决。
forward完美转发来在传参过程中保留原生类型属性,对左值引用不做处理,对右值引用,因为退化成了左值,所以将其强转为右值。
template<typename T>void PerfectForward(T&& t){Fun(forward<T>(t));cout << endl;}
这时会发现forward保留了原生类型,从而使接收右值的右值引用参数仍然具有右值属性。
从原理来看,forward是一个函数模板,结合我们自己的万能引用模板函数PerfectForward,根据传入的参数,被实例化成为了如下四个函数。通过使用模板的方式,这四种函数就可以省去不写,而由编译器根据模板完成。可以发现,move可以区别const和非const,得到const左、右值和非const左、右值。
void PerfectForward(int& t) //左值引用{Fun(t);}void PerfectForward(int&& t) //右值引用{Fun(move(t));}void PerfectForward(const int& t) //const 左值引用{Fun(t);}void PerfectForward(const int&& t) //const 右值引用 {Fun(move(t));}
完美转发用于传参时保留左右值属性,因此在面对向下调用函数需要根据左右值属性调用不同函数时使用,如我们 6.3.4 中的例子就可以使用完美转发来保证右值引用不退化。
7. 类功能更新
7.1 新增默认成员函数
在C++11中,除了6个默认成员函数,新增了两个默认成员函数:移动构造函数、移动赋值运算符重载。
这两个默认成员函数由编译器生成条件:①自己没有实现;②没有自己实现析构函数、拷贝函数、拷贝复制重载函数的任意一个。
可以理解为析构、拷贝构造、拷贝赋值、移动构造和移动赋值这五者同时出没。因为当对象申请资源了(这时需要深拷贝),它们都需要自主实现满足深拷贝和资源管理的要求。当没有资源申请,那么当前对象也就无需深拷贝,因此默认生成的成员函数足以满足,全部都无需自己实现了
对于默认生成的移动构造,对内置类型按字节拷贝,对自定义成员调用其移动构造,如果没有则调用拷贝构造。默认生成的移动赋值同理,对内置类型按字节拷贝,对自定义成员调用其移动赋值,如果没有则调用拷贝赋值。
通过默认生成的移动构造可以调用其自定义成员的移动构造,当自己是右值时,成员也是右值,尽管自身是浅拷贝不会由于移动构造而提高效率,但是成员有可能是深拷贝,采取移动构造可以大大提高效率。这也是默认移动构造的意义。
class Person{public:Person(const char* name = "", int age = 0):_name(name), _age(age){}private:C3::String::string _name;int _age;};//默认生成的移动构造和移动赋值对_name调用其移动构造和移动赋值,对_age直接字节拷贝void Test1(){Person s1;Person s2 = s1;Person s3 = std::move(s1);Person s4;s4 = std::move(s2);}
7.2 新增关键字
7.2.1 default
default可以强制生成默认成员函数,当需要使用某个默认的函数,但是因为一些原
因这个函数没有默认生成。比如:提供了拷贝构造,就不会生成移动构造了,此时就可以使用default强制生成移动构造。
Person(Person&& p) = default;
7.2.2 delete
delete可以禁止编译器生成默认成员函数,如禁止生成拷贝构造。
Person(const Person& p) = delete;
ostream类的拷贝构造就使用了delete关键字,而在C++11之前,想要阻止编译器生成默认成员函数,需要自己实现一个private属性的函数来限制编译器生成,同时保证无法在类外实现。
class A {//类内只声明不实现,限制编译器生成//声明为私有,防止使用者在类外实现public:A(){}private:A(const A& a);A& operator=(const A& a);private:int _a=0;};//不声明为私有就会在类外实现//A::A(const A& a)//{}
7.2.3 final
final在继承与多态中介绍过,它修饰的虚函数不可以再被重写。
virtual void func1() final
{}
7.2.4 override
override在继承与多态中介绍过,它修饰的虚函数会检查是否重写了父类的某个虚函数,如果没有重写则报错。
virtual void func2(int b=2) override
{}
8. 可变参数模板
8.1 可变参数模板的使用
在C语言中也有可变参数,如int printf ( const char * format, ... );。C语言的可变函数参数用...表示,可接收任意数量的参数。其底层是把参数放在了一个数组中。
对于传统的模板我们已经比较熟悉了,所谓函数模板就是给出一个函数的模型,而将其参数类型设置为可变值,根据模板实例化时提供的参数类型来由编译器生成指定参数类型的函数。
//一般的模板:参数个数固定,参数类型可变template <class T>void ShowList(T&& x){}
而可变参数模板则是更升一级,同样作为函数模板,它不仅参数的类型可变,并且参数的个数也是可变的。
//可变参数模板:参数个数可变,参数类型可变template<class ...Args> //表示Args是一个类型参数包,可以匹配任意数量的类型void ShowList(Args ...args) //表示args是一个函数形参参数包,可以接受任意数量的参数,参数的数量和类型由Args决定{}
其中Args是一个类型参数包,可以匹配任意数量的类型。
而args是一个函数形参参数包,可以接受任意数量的参数,参数的数量和类型由Args决定。
...根据其位置不同具有不同含义。
...出现在类型/参数名左侧 | 声明一个参数包 | 声明模板类型参数包Args:template <class ...Args> 声明函数形参参数包args:void ShowList(Args ...args) |
...出现在参数包名右侧 | 将参数包展开为具体参数 | 展开args函数形参参数包,得到的形参作为Print的参数:Print(args...); |
...与操作符相邻而与参数包分离 | 将参数包各个参数以指定的操作符展开计算 | 一元右折叠:( <参数包> <操作符> ... ) 适用右结合操作符。 如args=...展开为agrs[1]=(args[2]=(args[3]=args[4])) 一元左折叠:( ... <参数包> <操作符> ) 适用左结合操作符。 如...+args展开为((agrs[1]+args[2])+args[3])+args[4] 二元右折叠:( <参数包> <操作符> ... <操作符> <初值> ) 适用右结合操作符。 如args-...-2展开为agrs[1]-(args[2]-(args[3]-1)) 二元左折叠:( <初值> <操作符> ... <操作符> <参数包> ) 适用左结合操作符。 如2&...&args展开为((2&args[1])&args[2])&args[3] 注: ①一元或二元折叠视有无初值而定; ②左结合操作符采用左折叠,右结合操作符采用右折叠,这样满足操作符定义的计算方式,不会出错。可以混用,左右折叠控制求值顺序,但可能会导致运算符语法错误(报错)或不合常理运算逻辑(运算结果不合期望)。 |
8.2 展开参数包
如果我们在使用可变参数模板的过程中需要读取参数包内的参数值,就需要展开参数包。
8.2.1 运行时展开参数包(不可行)
模板在编译时完成实例化,所以可变模板参数会在编译时解析,实例化为需要的函数。以下的方式在运行时才进行解析参数,所以会失败。
template<class ...Args> //表示Args是一个类型参数包,可以匹配任意数量的类型void ShowList(Args ...args) //表示args是一个函数形参参数包,可以接受任意数量的参数,参数的数量和类型由Args决定{//std::cout << sizeof...(Args) << std::endl; //打印类型参数数量std::cout << sizeof...(args) << std::endl; //打印函数参数数量//模板在编译时完成实例化,所以可变模板参数会在编译时解析,实例化为需要的函数//以下的方式在运行时才进行解析参数,所以会失败for (size_t i = 0; i < sizeof...(args); i++){cout << args[i] << ' ';}}
8.2.2 递归函数展开参数包
编译时因为需要实例化模板,所以每次实例化一个Print函数都会解析出一个参数,由此递归完成解析。当参数全部被解析,会调用没有参数的Print(),即可走出递归。
//递归的出口,当参数全部被解析,会调用没有参数的Print(),即可走出递归void Print(){cout << endl;}//编译时因为需要实例化模板,所以每次实例化一个Print函数都会解析出一个参数,由此递归完成解析template <class T,class ...Args>void Print(T&& x, Args&&... args){cout << x << " ";Print(args...);}template<class ...Args>void ShowList(Args ...args){//编译时递归推导解析参数Print(args...);}
ShowList(1, 1.2, "abcd");
对于上面的调用,可以分析出Print函数被实例化为以下的函数,这也是能够完成参数包展开的原因。
void ShowList(int x, double y, char* z){Print(x, y, z);}void Print(int x, double y, char* z){cout << x << " ";Print(y, z);}void Print(double y, char* z){cout << y << " ";Print(z);}void Print(char* z){cout << z << " ";Print();}
8.2.3 数组展开参数包
用所有的参数分别调用PrintArg函数,在函数内会分别拿到各个参数,而函数返回int类型保证原数组语法正确。
template <class T>int PrintArg(T t){cout << t << " ";return 0;}template <class ...Args>void ShowList2(Args ...args){int arr[] = { PrintArg(args)... };cout << endl;}
ShowList2(1, 1.2, "abcd");
对于上面的调用,可以分析出Print函数被实例化为以下的函数,这也是能够完成参数包展开的原因。
void ShowList2(int x, double y, char* z){int arr[] = { PrintArg(x),PrintArg(x),PrintArg(x) };cout << endl;}int PrintArg(int t){cout << t << " ";return 0;}int PrintArg(double t){cout << t << " ";return 0;}int PrintArg(char* t){cout << t << " ";return 0;}
8.2.4 逗号表达式参数包展开
这种方法和依靠数组展开参数包异曲同工,利用逗号表达式的左结合性,其表达式的值是最右侧表达式的值。于是可以在解析参数包同时数组元素赋值为0,保证语法正确。
template <class ...Args>void ShowList2(Args ...args){int arr[] = { (cout << (args) << " ",0)... }; //利用逗号表达式,变为int arr[] = { (cout << (x) << " ",0),(cout << (y) << " ",0)...};//因为cout语句的返回值ostream对象不允许拷贝,所以使用逗号表达式让元素的值变为int的0cout << endl;}
8.3 emplace_back
8.3.1 emplace_back与push_back
以list为例。emplace_back同样是尾插操作,但是由于其支持万能引用,并且是可变参数模板,这会使得其效率进一步提高而胜过push_back。
std::list<String::string> lt;String::string s1("abc");//对于非临时对象,均调用拷贝构造,构造出新的对象放在list的容器内lt.push_back(s1);//拷贝构造lt.emplace_back(s1);//拷贝构造lt.emplace_back(move(s1));//移动构造//对于临时对象lt.push_back({ 10, 'a' });//构造+移动构造lt.emplace_back(10, 'a');//构造
可以发现,对于非临时对象,二者均调用拷贝构造,构造出新的对象放在list的容器内,所以没有效率差别。
而对于临时对象处理方法则不同。push_back首先调用string的构造函数,构造出一个临时对象,然后将这个临时对象传入push_back,由于是右值,所以最后采取移动构造完成list的尾插。emplace_back可以把构造string的参数包(10, 'a')向下传递,然后直接使用该参数包将对象构造在list容器内。相比之下可以发现,emplace_back相较于push_back少了一次移动构造的开销。
所以可以发现,emplace_back在面对非临时对象时与push_back效率相当,而面对临时对象则是有效率提升,因而以后的插入操作完全可以使用emplace_back全面代替push_back。
//emplace_back会直接在容器内存中构造对象lt.emplace_back(10, 'a'); 对于这种添加元素方法//比下文节约了一次拷贝构造String::string s1(10, 'a');lt.push_back(s1);//比下文节约了一次移动构造lt.push_back({ 10, 'a' });
8.3.2 emplace_back的实现
在过去的list代码的基础上进行补充。emplace_back调用过程中参数包无需途中解析,一直传递到最后的元素构造处,即emplace_back->insert->ListNode->string。在此过程中,为了避免其右值引用退化为左值(6.3.4的经验),使用完美转发保证左右值属性的传递。
// emplace_back调用过程中参数包无需途中解析,一直传递到最后的元素构造处// 实际是包括emplace_back、insert、ListNode构造都根据参数包实例化出了函数template <class... Args>void emplace_back(Args&&... args){insert(end(), forward<Args>(args)...);//向insert传递参数包,使用完美转发保证左右值属性的传递}
template <class... Args>void insert(iterator pos, Args&&... args){Node* next = pos._node;Node* prev = next->_prev;Node* newnode = new Node(forward<Args>(args)...);//向Node构造传递参数包prev->_next = newnode;newnode->_prev = prev;newnode->_next = next;next->_prev = newnode;++_size;}
template <class... Args>ListNode(Args&&... args): _prev(nullptr), _next(nullptr), _val(forward<Args>(args)...) //向_val构造传递参数包{}
整个过程实际是包括emplace_back、insert、ListNode构造都根据参数包实例化出了函数。
9. lambda表达式
9.1 引例
在C++的algorithm头文件下,有用于排序的库函数,通过提供开始和结束位置的迭代器即可完成排序操作,为了控制排序逻辑,sort有第三个参数用于接收一个用于比较的比较函数对象。
对于如下的一个类:
struct Goods {string _name;double _price;int _evaluate;Goods(const char* str, double price, int evaluate):_name(str), _price(price), _evaluate(evaluate){}};vector<Goods> v1 = { {"apple",2.2,4},{"chocolate",8.8,2},{"ice-cream",4.5,3},{"book",22.1,5} };
按照我们以往的知识想要对数组v1排序并不难。
sort(v1.begin(), v1.end(), greater<Goods>());//比较方式:g1>g2,即调用Goods类的大于号运算符重载函数
//由于Goods类的大于号重载函数只能有一个,所以相当于确定了排序逻辑按姓名降序,如果要更改排序逻辑则需要修改大于号运算符重载函数实现
9.1.1 运算符重载
第一种方法就是直接调用库中提供的greater或less模板。以greater为例,库中提供的比较模板的操作是直接采取大于号运算符进行大小判断,所以我们把greater模板实例化为Goods类后,greater实际是在对两个Goods对象直接使用大于号进行比较。这就要求Goods类要对>运算符进行重载。
//在类内重载大于小于比较符bool operator>(const Goods& g)const{return _name > g._name;}
很明显,由于Goods类的大于号重载函数只能有一个,所以相当于确定了排序逻辑按姓名降序,如果要更改排序逻辑则需要修改大于号运算符实现。但是在实际应用场景中应该支持任意一个属性的升序降序排序,这就需要我们使用其他的方式。
9.1.2 仿函数
第二种方法是采用仿函数。仿函数的概念我们曾经解释过,仿函数实际上是一个类,只是这个类重载了()运算符,可以通过任意一个类对象调用括号运算符完成制定工作。仿函数类的属性无关紧要,其最大的价值就是重载了括号,因为其调用括号操作符的方式是 对象(参数) ,与函数调用的形式很类似,所以称为仿函数。在使用时仅需构造一个类对象,甚至可以是匿名对象,因为对象的作用仅仅是为了调用函数,不关心其属性。
struct ComparePriceLess {bool operator()(const Goods& g1, const Goods& g2){return g1._price < g2._price;}};struct ComparePriceGreater {bool operator()(const Goods& g1, const Goods& g2){return g1._price > g2._price;}};
以上是根据价格属性比较的仿函数。
sort(v1.begin(), v1.end(), ComparePriceLess());//调用ComparePriceLess仿函数作为比较逻辑
sort(v1.begin(), v1.end(), ComparePriceGreater());//调用ComparePriceGreater仿函数作为比较逻辑
因为sort第三个参数要求传递一个用于比较的比较函数对象,而仿函数ComparePriceLess()实际上是一个匿名类对象,在sort的实现中调用ComparePriceLess类的()操作符重载作为比较逻辑。
9.2 lambda表达式语法
仿函数可以很好地解决上面的问题了,但是会发现想要对每一种类成员都写一份仿函数是比较麻烦的,因此引入lambda表达式。
lambda表达式语法:[capture-list] (parameters) mutable -> return-type { statement }
lambda表达式可以被称作“匿名函数”,因为其有着函数的功能和调用方法,却没有一个函数名。
9.2.1 简单使用方法
lambda表达式中,方括号[]表示捕捉列表,大括号{}表示函数体,这两部分是不可以省略的。
圆括号()表示参数,在使用lambda表达式需要传入参数;在函数体前是返回类型,如果没有返回值可以写作void。
在定义好lambda表达式后,需要赋给一个变量用于后续的调用操作。这个变量的类型使用auto自动推导即可,具体类型其实是一个名为lambda_uuid的类,可以将其也理解为仿函数。后续只需像调用函数一样使用这个变量以及传入参数即可调用lambda表达式。
auto add = [](int x, int y)->int {return x + y; };cout << add(1, 2) << endl;auto fun1 = []()->void{cout << "hello world";cout << endl;};fun1();//->返回类型 无论有无返回值,这一部分都可以被省略,如果有返回值并且省略了,编译器会自动推导类型auto fun2 = [](){cout << "hello world";cout << endl;return 2;};cout << fun2() << endl;//(参数列表)->返回类型 当没有参数时这一部分可以一起省略auto fun3 = [] {cout << "π="; return 3.14; };cout << fun3() << endl;//auto fun2 = []->int { return 2; }; //error 就算没有参数,()也不能单独被省略,要省略()需要先省略->和返回类型
需要指出:
①->返回类型 无论有无返回值,这一部分都可以被省略,如果有返回值并且省略了,编译器会自动推导类型。
②(参数列表)->返回类型 当没有参数时这一部分可以一起省略。
③就算没有参数,()也不能单独被省略,要省略()需要先省略->和返回类型。
于是我们就可以使用lambda表达式解决引例的问题了。
//使用lambda表达式作为sort的第三个参数//评价升序sort(v1.begin(), v1.end(), [](const Goods& g1,const Goods& g2){return g1._evaluate < g2._evaluate;});//评价降序sort(v1.begin(), v1.end(), [](const Goods& g1, const Goods& g2){return g1._evaluate > g2._evaluate;});
9.2.2 捕捉列表[capture-list]与mutable
int a = 0, b = 1;auto swap1 = [](int& x, int& y){//a;//error 在lambda函数体中,只能使用挡圈lambda局部域的对象和捕捉的对象和全局对象int tmp = x;x = y;y = tmp;};swap1(a, b);
在lambda函数体中,只能使用当前lambda局部域的对象和捕捉的对象和全局对象,如果想要使用外部的对象,就需要用到捕捉列表。
9.2.2.1 传值捕捉
传值捕捉,捕捉变量的本质是外部变量的拷贝,并且被const修饰,如下例捕捉到的a,b对象在lambda函数体重可以视为是具有const修饰的右值,所以无法更改。
auto swap2 = [a, b]{int tmp = a;a = b; //error //捕捉到的a,b对象在lambda函数体重可以视为是具有const修饰的右值,所以无法更改b = tmp;};
如果希望谢盖捕捉到的变量,可以使用mutable,这相当于去除了const属性。尽管使用了mutable后可以修改捕捉的变量但是,因为是拷贝所以修改不会影响外部的值,这和修改形参实参不会受影响是一个道理。
需要注意使用mutable时()就算没有参数也不可以省略。
//mutable相当于去除const属性,可以修改变量//因为是拷贝所以修改不会影响外部的值auto swap2 = [a, b]()mutable //使用mutable可以取消常量性变为非const,使用mutable时()就算没有参数也不可以省略{int tmp = a;a = b;b = tmp;};swap2();//传值捕捉不修改a、b
mutable一般不使用,因为仅在函数体内改变拷贝的值没有意义,如果想影响外部变量应该使用传引用捕捉,这也是传值捕捉有const限制而传引用捕捉没有的原因。
9.2.2.2 传引用捕捉
传引用捕捉,捕捉变量的是外部变量的引用,没有附加const,可以修改捕捉的变量来改变外部的变量。
//传引用捕捉,捕捉变量的是外部变量的引用,没有附加const
auto swap3 = [&a, &b]{int tmp = a;a = b;b = tmp;};
swap3();//传引用捕捉可以修改a、b
9.2.2.3 其他捕捉方式
其余还包括所有值传值捕捉、所有值传引用捕捉、混合捕捉。
//所有值传值捕捉,捕捉所有外部变量,如果在成员函数内this也会被捕捉auto fun1 = [=]{return a + b + c + d;};fun1();//所有值传引用捕捉,捕捉所有外部变量,如果在成员函数内this也会被捕捉auto fun2 = [&]{a++;b++;c++;d++;};fun2();//混合捕捉auto fun3 = [&a,b]{a++;b;};fun3();auto fun4 = [&, b] //所有值传引用捕捉,仅b传值捕捉{a++;b;c++;d++;};fun4();}
9.3 总结
由此再回头看lambda确实像一个“匿名函数”,它没有函数名,因为自身只是一个表达式,所以需要使用的时候随时随地创建即可。
lambda的底层实际上完全就是按照函数对象的方式处理的,即:如果定义了一个lambda表达式,编译器会自动生成一个类,在该类中重载了operator()。
而捕捉列表的对象会以成员变量的形式存在于lambda类对象中。所以:
①定义lambda实际上是定义一个仿函数类;
②捕捉实际上是构造仿函数类的初始化参数;
③调用lambda实际上是调用仿函数类的operator()。
10.包装器
10.1 function
10.1.1 对函数指针、仿函数、lambda的包装
function包装器,C++中的function本质是一个类模板,它的作用是包装所有的可调用对象,如函数指针、仿函数、lambda。
对于function,其模板实例化有一些特殊,需要根据包装的可调用对象的返回类型和参数类型,以 <返回类型 ( 参数类型 ) > 的形式进行实例化。
function的很重要的作用一方面是以包装的方式为各种可调用对象赋予相同的调用方法,另一个方面就是可以统一相同返回类型、相同参数类型的调用对象。
int add(int a, int b){return a + b;}struct Sub {int operator()(int a, int b){return a - b;}};void Test1(){function<int(int, int)>f1 = add;function<int(int, int)>f2 = Sub();function<int(int, int)>f3 = [](int a, int b) {return a * b; };cout << f1(1, 2) << ' ' << f2(2, 3) << ' ' << f3(3, 4) << endl;//统一相同返回类型、相同参数类型的调用对象map<string, function<int(int, int)>> opFuncMap({{ "+",f1 }, { "-",f2 }, { "*", f3 }});cout << opFuncMap["+"](1, 2) << ' ' << opFuncMap["-"](3, 4) << ' ' << opFuncMap["+"](5, 6) << endl;}
10.1.2 对成员函数的包装
对于静态成员,指明其函数时需要指定类域,除此之外和一般的函数包装相同。
而对于非静态成员函数,除了指明类域外还需要注意其存在隐藏的参数this*。为了处理这个this指针参数,有两种常见的方式,一种是使用类对象指针作为参数,另一种是使用类对象作为参数。对于这两种方法,function将可调用对象包装成了一个仿函数类,通过operator()去调用可调用对象,而传递的类对象指针或类对象参数本质上都是用于operator()调用非静态成员函数的,所以都可行。
除此之外静态成员函数的取地址可以不需要&,而普通成员函数取地址一定需要&,为统一可以认为他们都要使用&来取地址。
void Test2(){//包装静态成员函数function<int(int, int)> f1 = &A::addi;// 需要指定类域cout << f1(1, 2) << endl;//静态成员函数的取地址可以不需要&,而普通成员函数取地址一定需要&,为统一可以认为他们都要使用&来取地址//包装非静态成员函数,注意存在隐藏的参数this*//方法1function<double(A*, double, double)> f2 = &A::addd;// 需要指定类域A a;cout << f2(&a, 1.3, 2.4) << endl;//方法2function<double(A, double, double)> f3 = &A::addd;// 需要指定类域cout << f3(a, 1.3, 2.4) << endl;cout << f3(A(), 1.3, 2.4) << endl;//匿名对象//function将可调用对象包装成了一个仿函数类,通过operator()去调用可调用对象//而传递的类对象指针或类对象参数本质上都是用于operator()调用非静态成员函数的,所以都可行}
10.2 bind
bind是一个函数模板,它就像一个函数包装器(适配器),接受一个可调用对象,生成一个新的可调用对象来“适应”原对象的参数列表。在bind对可调用对象绑定完成后,最后得到也是一个仿函数对象。
bind一般具有两种用法:调整参数顺序和调整参数个数。
bind的使用和placeholders息息相关。在命名空间placeholders中,_n表示第n个位置的实参。
可以通过_1、_2……等控制参数传递的顺序。而有关参数对应关系的辨别,在使用过程中仅需谨记:bind的可变参数列表顺序和可调用对象参数顺序一致,调用bind绑定后的可调用对象接收实参的顺序取决于_n。
在定义bind时可以将某些参数置为一个固定值,这样就会固定使用该值进行传参,从而可以减少接收的参数个数,达到调整参数个数的效果。
int Sub(int a, int b)
{return a - b;
}
//在命名空间placeholders中,_n表示第n个位置的实参
using placeholders::_1;
using placeholders::_2;
using placeholders::_3;//bind的可变参数列表顺序和可调用对象参数顺序一致
//调用bind绑定后的可调用对象接收实参的顺序取决于_n
void Test3()
{//调整参数顺序auto sub1 = bind(Sub, _1, _2);cout << sub1(3, 2) << endl;auto sub2 = bind(Sub, _2, _1);cout << sub2(3, 2) << endl; //在这种情况下, 第一个实参3会传递给_1,_1位于bind的可变参数列表的第二个位置,所以传递给Sub的第二个形参b// 第二个实参2传递给_2,_2位于bind的可变参数列表的第一个位置,所以传递给Sub的第一个形参a//调整参数个数auto sub3 = bind(Sub, 100, _1);cout << sub3(3, 2) << endl;//第一个实参3传给_1,_1传递给第二个形参b。第二个实参无处传递。被固定的参数100传递给第一个形参a
}
然而上述内容都是手段,bind的使用目的一般是想用于绑死某些固定参数。如对类内非静态成员函数的function包装后需要一个类对象的参数应付this指针。于是可以通过bind绑定A类的成员函数&A::addd,其接收的第一个参数固定为A(),第二个参数为调用时传递的第一个实参,第三个参数为调用时传递的第二个实参。bind绑定后是一个可调用对象(仿函数),再使用function进行包装为fun。如上这样包装后调用方法就被简化了,不必再传递类对象。
//bind一般用于绑死某些固定参数//bind绑定A类的成员函数&A::addd,其接收的第一个参数固定为A(),第二个参数为调用时传递的第一个实参,第三个参数为调用时传递的第二个实参//bind绑定后是一个可调用对象(仿函数),再使用function进行包装为funfunction<double(double, double)> fun = bind(&A::addd, A(), _1, _2);cout << fun(1.1 , 2.2) << endl;