【C++11】尽显锋芒
(续)
一、可变参数模板
C++11支持可变参数模板,也就是说支持可变数量参数的函数模板和类模板,可变数目的参数被称
为参数包,存在两种参数包:模板参数包,表示零或多个模板参数;函数参数包:表示零或多个函
数参数。
template <class ...Args> void Func(Args... args) {} //传值
template <class ...Args> void Func(Args&... args) {} //左值引用
template <class ...Args> void Func(Args&&... args) {} //万能引用
我们用省略号来指出⼀个模板参数或函数参数的⼀个包。在模板参数列表中,class.../typename...指出接下来的参数表述零或多个类型列表;在函数参数列表中,类型名后面跟...指出接下来表示零或多个形参对象列表;函数参数包可以用左值引用或右值引用表示,跟前面普通模板一样,每个参数实例化时遵循引用折叠规则。
我们来看一个例子:
//参数包表示0-N个参数
template<class ...Args>
void Print(Args&&... args) //args是一个参数包
{//这里的sizeof...可以认为是一个新的运算符,专门用来计算参数包中参数的个数,它与sizeof的功能是不一样的cout << sizeof...(args) << endl;
}
int main()
{double x = 2.2;Print(); //包里有0个参数Print(1); //包里有1个参数Print(1, string("xxxxx")); //包里有2个参数Print(1.1, string("xxxxx"), x); //包里有3个参数return 0;
}
可变参数模板的原理跟模板类似,本质还是去实例化对应类型和个数的多个函数。
若没有可变参数模板,像上面的代码,我们需要写4个函数模板:
void Print()
{}template<class T1>
void Print(T1&& x1)
{}template<class T1,class T2>
void Print(T1&& x1,T2&& x2)
{}template<class T1, class T2,class T3>
void Print(T1&& x1, T2&& x2, T3&& x3)
{}
现在有了可变参数模板,那么上述的任务统统交给编译器,编译器会帮助我们生成上述的4个模板参数然后依次调用,而我们的任务就是写一个可变参数模板即可。
可变参数模板带来的效果:
//根据Print调用情况
//1、首先生成4个函数模板
void Print() {}template <class T1>
void Print(T1&& arg1)
{}template <class T1, class T2>
void Print(T1&& arg1, T2&& arg2)
{}template <class T1, class T2, class T3>
void Print(T1&& arg1, T2&& arg2, T3&& arg3)
{}//2、结合引⽤折叠规则实例化出以下四个函数
void Print()
{}void Print(int&& arg1)
{}void Print(int&& arg1, string&& arg2)
{}void Print(double&& arg1, string&& arg2, double& arg3) //x是左值,所以arg3的类型是左值引用(引用折叠)
{}
可以将可变参数模板理解为模板的模板。
总结:
模板:一个函数模板可以实例化出多个不同类型参数的函数(类型可变)
可变参数模板:一个可变参数模板函数可以实例化出多个不同参数个数的函数模板(类型可变+个数可变)
二、包扩展
通过上面的学习,我们可以将包中的参数个数打印出来,那能不能将包中的内容打印出来呢(就是把参数取出来)?
思路:
template<class ...Args>
void Print(Args&&... args)
{for (int i = 0;i < sizeof...(args);++i)cout << args[i] << " "; cout << endl;
}
这样写法看上去可以,但是,它是C++是不支持这样写的,有一点就直接否定了:包中每个参数的类型不同。类型都不同怎么可能像数组这样使用(args[i]),数组也必须保证里面的元素类型相同。所以这种方法是不可取的。
解决方法:包扩展(解析出参数包的内容)
方式一、
//包扩展(解析出参数包的内容)
//方式一、
void ShowList() //参数包中参数个数为0,直接匹配这个函数
{cout << endl;
}template <class T, class ...Args>
void ShowList(T&& x, Args&&... args)
{cout << x << " "; //args是N个参数的参数包,打印参数包第一个参数ShowList(args...); //调用ShowList,将参数包中剩下N-1个参数传过去
}template <class ...Args>
void Print(Args&&... args)
{ShowList(args...); //注意实参传的形式
}int main()
{double x = 2.2;Print(); //包里有0个参数Print(1); //包里有1个参数Print(1, string("xxxxx")); //包里有2个参数Print(1.1, string("xxxxx"), x); //包里有3个参数return 0;
}
运行结果:
我简单来说一下这段代码的执行过程:
首先,执行Print()时,参数包中参数个数为0,那么就会直接调用void ShowList()这个函数,打印'\n';执行Print(1),参数包中参数个数为1,会调用void ShowList(T&& x, Args&&... args)这个函数模板,首先打印第一个参数内容,然后调用ShowList(args...),此时args这个参数包中参数个数就像对于之前减少了1,那就是0,于是调用void ShowList(),打印'\n',结束;执行Print(1, string("xxxxx")),参数包中参数个数为2,会调用void ShowList(T&& x, Args&&... args)这个函数模板,首先打印第一个参数内容,然后调用ShowList(args...),此时args这个参数包中参数个数就像对于之前减少了1,那就是1,于是再调用void ShowList(T&& x, Args&&... args),打印第一个参数内容(原参数包第二个参数的内容),然后调用ShowList(args...),此时args这个参数包中参数个数就像对于之前减少了1,那就是0,于是调用void ShowList(),打印'\n',结束;执行Print(1.1, string("xxxxx"), x)的过程和上面类似。
上述描述就是包展开的过程,包展开的过程是在程序编译阶段完成的,并非运行时。
为什么说是编译阶段完成的呢?
因为ShowList(T&& x, Args&&... args)这是一个函数模板,函数模板在确定类型时是在编译阶段完成的,所以,包展开的过程是在程序编译阶段完成的。
我们结合示意图来理解一下:
图中右边出现的函数统统在编译阶段由编译器实例化出来的!!!然后程序运行直接调用实例化出来的函数。
有人会觉得上述逻辑有点麻烦,可以这样写:
template <class T, class ...Args>
void ShowList(T&& x, Args&&... args)
{if (sizeof...(args) == 0)return;cout << x << " ";ShowList(args...);
}
但是,这样写是错误的,包展开的过程是在编译阶段完成的,而if判断这句代码是在程序运行时才执行的,所以这样写的逻辑是不对的。
我们也可以不用模板,直接主动写出具体函数:
void ShowList()
{cout << endl;
}void ShowList(double x)
{cout << x << " ";ShowList();
}void ShowList(string x, double z)
{cout << x << " ";ShowList(z);
}void ShowList(int x, string y, double z)
{cout << x << " ";ShowList(y, z);
}void Print(int x, string y, double z)
{ShowList(x, y, z);
}int main()
{Print(1, string("xxxxx"), 2.2);return 0;
}
运行结果:
上述写法就是我们自己写,程序运行直接调用即可;如果我们写的是模板,那么执行"Print(1, string("xxxxx"), 2.2)"时,编译器就会在编译阶段将可变参数模板通过模式的包扩展,推导出上面三个重载函数(ShowList),也就是编译器在底层帮我们实现并调用。
所以,我们在写代码时只用写一个模板即可,剩下的工作交给编译器,编译器所做的工作量是巨大的,因为它需要通过模板来实例化出实际有意义的函数,模板的作用就是减少了我们的工作量,增加的编译器的工作量,(编译器"累点"没关系,我们轻松就行^____^)。模板是不会改变效率问题的,也可以将模板理解为:"模板是写给编译器的"。
方式二、
//包扩展(解析出参数包的内容)
//方式二、
template <class T>
int GetArg(const T& x)
{cout << x << " ";return 0; //任意返回
}template <class ...Args>
void Arguments(Args... args) //充当"跳板"
{}template <class ...Args>
void Print(Args... args)
{//注意GetArg必须有返回值,这样才能组成参数包给ArgumentsArguments(GetArg(args)...); //注意语法格式
}int main()
{Print(1, string("xxxxx"), 2.2);return 0;
}
运行结果:
这种方式不会有发生递归, 利用Arguments当"跳板",执行3次GetArg,即可解析出args包里的内容。Arguments必须存在,Arguments存在,编译器就需要对其参数个数进行推导,一旦推导,那GetArg就会将包中的数据统统解析(打印)出来。
编译器会将Print处理为如下结果:
void Print(int x, string y, double z)
{Arguments(GetArg(x), GetArg(y), GetArg(z));
}
其实Print也可以这样写:
template <class ...Args>
void Print(Args... args)
{int arr[] = { GetArg(args)... };
}
如果这样写,要保证GetArg的返回类型是整形;因为要推导出arr数组到底有多大,就必须将包里的数据遍历完。
三、emplace系列接口
C++11以后STL容器新增了emplace系列的接口,emplace系列的接口均为可变参数模板,如emplace_back,它的功能与push_back类似,也是插入数据,但是它们之间有不同的地方。
我们以list容器为例(每个容器基本都有emplace_back接口):
我们来看看这两个接口到底有什么不同的地方:
外部条件:emplace_back调用时需要传一个参数包;push_back调用时需要传一个对象。
内部调用:
我们先自己写一个string,方便后续观察现象:
namespace blue
{class string{public:typedef char* iterator;typedef const char* const_iterator;iterator begin() {return _str;}iterator end() {return _str + _size;}const_iterator begin() const {return _str;}const_iterator end() const {return _str + _size;}//构造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){cout << "string(const string& s) -- 拷贝构造" << endl;reserve(s._capacity);for (auto ch : s)push_back(ch);}void swap(string& ss){std::swap(_str, ss._str);std::swap(_size, ss._size);std::swap(_capacity, ss._capacity);}//移动构造string(string&& s){cout << "string(string&& s) -- 移动构造" << endl;// 转移掠夺你的资源swap(s);}//赋值重载string& operator=(const string& s){cout << "string& operator=(const string& s) -- 赋值重载" <<endl;if (this != &s){_str[0] = '\0';_size = 0;reserve(s._capacity);for (auto ch : s)push_back(ch);}return *this;}//移动赋值string& operator=(string&& s){cout << "string& operator=(string&& s) -- 移动赋值" << endl;swap(s);return *this;}~string(){//cout << "~string() -- 析构" << endl;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];if (_str){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;}size_t size() const {return _size;}private:char* _str = nullptr;size_t _size = 0;size_t _capacity = 0;};
}
通过接口调用来观察它们两者的区别:
调用形式一:
int main()
{list<blue::string> lt;blue::string s1("111111111111");cout << "--------------------------" << endl;//传左值,emplace_back和push_back一样,走拷贝构造,两者没有区别lt.emplace_back(s1);cout << "--------------------------" << endl;lt.push_back(s1);cout << "--------------------------" << endl;return 0;
}
运行结果:
传左值,emplace_back和push_back一样,走拷贝构造,两者在效率方面没有任何区别。
调用形式二:
int main()
{list<blue::string> lt;blue::string s1("111111111111");cout << "--------------------------" << endl;//传右值,emplace_back和push_back一样,走移动构造,两者没有区别lt.emplace_back(move(s1));cout << "--------------------------" << endl;lt.push_back(move(s1));cout << "--------------------------" << endl;return 0;
}
运行结果:
传右值,emplace_back和push_back一样,走移动构造,两者没有区别。
调用形式三:
int main()
{list<blue::string> lt;blue::string s1("111111111111");cout << "--------------------------" << endl;lt.emplace_back("111111111111"); //不走隐式类型转换cout << "--------------------------" << endl;lt.push_back("111111111111"); //直接传参,走隐式类型转换cout << "--------------------------" << endl;return 0;
}
运行结果:
这时,它们两个的调用结果就不一样了。
对于push_back来说:
在实例化lt时就确定了value_type是blue::string类型,所以调用push_back时,value_type此时就是blue::string类型,而我们的参数是const char*,参数不匹配,所以首先要走隐私类型转换,那么就要调blue::string的构造函数 ,生成临时对象,临时对象是右值,所以会调用void push_back (value_type&& val),此时的val就是右值引用,然后一层一层往下传,最终会调用blue::string的移动构造。
对于emplace_back来说:
在实例化lt时,Args是什么类型是无法确定的,只有传参时才可推导出Args的具体类型,所以调用emplace_back时,不会隐式类型转换,Args此时的类型是const char*,然后层层向下传,最终调用blue::string的构造。
所以,对于这种情况,emplace_back和push_back是有差别的,push_back比emplace_back多了一个移动构造,效率方面其实影响也不大,因为移动构造的代价极低。但是,但是如果对于浅拷贝的类型呢?比如list中的元素类型是Date,Date类中没有移动构造和移动赋值(也不需要),那么push_back就会比emplace_back多了一个拷贝构造,那么emplace_back会更快一点点。注意:这里所说的快一点点,其实可以忽略不计,效率几乎不会受到影响。
调用形式四:
int main()
{list<pair<blue::string, int>> lt1;pair<blue::string, int> kv("苹果", 1);cout << "--------------------------" << endl;//传左值,emplace_back和push_back一样,走拷贝构造,两者没有区别lt1.emplace_back(kv);cout << "--------------------------" << endl;lt1.push_back(kv);cout << "--------------------------" << endl;return 0;
}
运行结果:
传左值,emplace_back和push_back一样,走拷贝构造,两者在效率方面没有任何区别。
调用形式五:
int main()
{list<pair<blue::string, int>> lt1;pair<blue::string, int> kv("苹果", 1);cout << "--------------------------" << endl;//传右值,emplace_back和push_back一样,走移动构造,两者没有区别lt1.emplace_back(move(kv));cout << "--------------------------" << endl;lt1.push_back(move(kv));cout << "--------------------------" << endl;return 0;
}
运行结果:
传右值,emplace_back和push_back一样,走移动构造,两者没有区别。
调用形式六:
int main()
{list<pair<blue::string, int>> lt1;pair<blue::string, int> kv("苹果", 1);cout << "--------------------------" << endl;//lt1.emplace_back({ "苹果", 1 }); //不支持,编译器无法推导出Args具体类型lt1.emplace_back("苹果", 1 ); //参数包,层层往下传,最终调用构造生成结点(因为pari支持2参构造)cout << "--------------------------" << endl;//lt1.push_back("苹果", 1); //不支持lt1.push_back({ "苹果", 1 }); //隐式类型转换,先构造pair的临时对象,最后层层下传最终调用移动构造生成结点cout << "--------------------------" << endl;return 0;
}
运行结果:
所以相较于插入来说,还是emplace_back要比push_back略快一些。
总结:
emplace系列兼容push系列和insert的功能,部分场景下emplace可以直接构造,push和insert是构造+移动构造或构造+拷贝构造,所以emplace综合而言更好用、更强大。
故推荐emplace系列接口替代push和insert系列接口。
四、lambda表达式
🐱🏍基本介绍
lambda表达式本质是⼀个匿名函数对象,跟普通函数不同的是它可以定义在函数内部。
lambda表达式语法对使用层而言没有类型,所以我们一般使用auto或者模板参数定义的对象去接收lambda对象。
lambda表达式的语法格式:
[capture-list] (parameters)-> return type { function boby }
[capture-list]:捕捉列表,该列表总是出现在lambda表达式的开始位置,编译器根据[]来判断接下来的代码是否为lambda表达式,捕捉列表能够捕捉上下文中的变量供lambda函数使用,捕捉列表可以传值和传引用捕捉,捕捉列表可以为空,但捕捉列表在任何情况下都不可省略。
(parameters):参数列表,与普通函数的参数列表功能类似;如果不需要参数传递,则可以连同()⼀起省略。
->return type:返回值类型,用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可省略。⼀般返回值类型明确情况下,也可省略,由编译器对返回类型进行推导。
{function boby}:函数体,函数体内的实现跟普通函数完全类似;在该函数体内,除了可以使用参数列表的参数外,还可以使用所有捕获到的变量,函数体可以为空,但函数体在任何情况下都不可省略。
我们先来写一个简单的lambda表达式来帮助理解:
int main()
{//用auto自动推导add1类型即可,我们无法知道add1的具体类型名,也不需要知道// 捕获列表 参数列表 返回类型 函数体auto add1 = [](int x, int y)->int { return x + y; }; //add1是一个lambda对象cout << add1(1, 2) << endl;return 0;
}
运行结果:
是不是也没有那么复杂?我们再来写一个:
int main()
{//1、捕捉列表为空也不能省略//2、参数列表为空可以省略//3、返回值可以省略,可以通过返回对象自动推导//4、函数体不能省略auto func1 = []{cout << "hello bit" << endl;return 0;};func1();return 0;
}
运行结果:
通过上面两个例子,相信大家已经了解了lambda的基本使用方法,接下来,我们来看一看捕捉列表到底有什么作用。
🐱🏍捕捉列表
lambda表达式中默认只能用lambda函数体和参数列表中的变量,如果想用外层作用域中的变量就
需要进行捕捉。
🐱👤捕捉方式一:
第一种捕捉方式是在捕捉列表中显示的传值捕捉和传引用捕捉,捕捉的多个变量用逗号分隔;比如:[x,y,&z],其中x和y是值捕捉,z是引用捕捉。通常情况下,引用都是和类型绑在一起的,&z大家第一眼可能认为是取地址,但在捕捉列表中&表示引用,这点是特殊的,大家不要记错。
使用值捕捉的变量不能被修改(默认被const修饰)。
如果将a、b定义到lambda表达式下面,会报错,因为编译器走到捕捉列表时,只会向上查找,不会向下查找。
特殊地,在类域中,若要使用某个变量会在整个类域中查找。
在lambda的函数体中可以直接使用全局域的东西,不需要通过捕捉列表捕捉,也不能捕捉:
注意:同一个变量不能捕捉两次。
🐱👤捕捉方式二:
第二种捕捉方式是在捕捉列表中隐式捕捉,我们在捕捉列表写一个=表示隐式值捕捉,在捕捉列表写⼀个&表示隐式引用捕捉,这样我们lambda表达式中用了哪些变量,编译器就会自动捕捉那些变
量。
int main()
{ int a = 0, b = 1, c = 2, d = 3;//隐式值捕捉 - 用了哪些变量就捕捉那些变量auto func2 = [=]{//a++; //err,值捕捉就不能修改捕捉到的变量int ret = a + b + c;return ret;};cout << func2() << endl;//隐式引用捕捉 - 用了哪些变量就捕捉哪些变量auto func3 = [&]{//引用捕捉可以修改捕捉到的变量a++;c++;d++;//e++; //err,必须确保使用的变量能被捕捉到,否则就报错};func3();return 0;
}
🐱👤捕捉方式三:
第三种捕捉方式是在捕捉列表中混合使用隐式捕捉和显示捕捉。[=,&x]表示其它变量隐式值捕捉,
x引用捕捉;[&,x,y]表示其它变量引用捕捉,x和y值捕捉。当使用混合捕捉时,第⼀个元素必须是&或=,并且&混合捕捉时,后面的捕捉变量必须是值捕捉,同理=混合捕捉时,后面的捕捉变量必须是引用捕捉。
int main()
{int a = 0, b = 1, c = 2, d = 3;//混合捕捉1auto func4 = [&, a, b]{//a、b是值捕捉,故不能修改//a++;//b++;c++;d++;return a + b + c + d;};cout << func4() << endl;//混合捕捉2auto func5 = [=, &a, &b]{a++;b++;//c、d是值捕捉,故不能修改//c++;//d++;return a + b + c + d;};cout << func5() << endl;return 0;
}
注意:
lambda表达式如果在函数局部域中,它可以捕捉lambda位置之前定义的变量,但不能捕捉静态
局部变量和全局变量,静态局部变量和全局变量也不需要捕捉, lambda表达式中可以直接使用。这也意味着lambda表达式如果定义在全局位置,捕捉列表必须为空(因为没有东西能捕捉)。
默认情况下,传值捕捉的过来的对象不能被修改(被const修饰),但将mutable加在参数列表的后面可以取消其常量性,也就说使用该修饰符后,传值捕捉的对象就可以修改了,但是修改还是针对形参对象,不会影响外面实参;使用该修饰符(mutable)后,参数列表不可省略(即使参数列表为空)。
🐱🏍lambda的应用
在学习lambda表达式之前,我们使用的可调用对象只有函数指针和仿函数对象,函数指针的类型定义起来比较麻烦,仿函数要定义⼀个类,相对会比较麻烦。使用lambda去定义可调用对象,既简单又方便,例如:
struct Goods
{string _name; //名字double _price; //价格int _evaluate; //评价Goods(const char* str, double price, int evaluate):_name(str), _price(price), _evaluate(evaluate){}
};
struct Compare1
{bool operator()(const Goods& gl, const Goods& gr){return gl._price < gr._price;}
};
struct Compare2
{bool operator()(const Goods& gl, const Goods& gr){return gl._price > gr._price;}
};
int main()
{vector<Goods> v = { { "苹果", 2.1, 5 }, { "⾹蕉", 3, 4 }, { "橙⼦", 2.2, 3}, { "菠萝", 1.5, 4 } };//类似这样的场景,我们实现仿函数对象或者函数指针支持商品中不同项的比较,相对还是比较麻烦的,而且如果命名风格不好,代码可读性也会降低sort(v.begin(), v.end(), Compare1());sort(v.begin(), v.end(), Compare2());//那么这里lambda就很好用了(如果是仿函数,就需要写上4个),这里并不需要担心命名风格,因为lambda表达式可以直观的看出来具体功能sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {return g1._price < g2._price;}); //按价格升序排列sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {return g1._price > g2._price;}); //按价格降序排列sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {return g1._evaluate < g2._evaluate;}); //按评价升序排列sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {return g1._evaluate > g2._evaluate;}); //按评价降序排列return 0;
}
lambda在其它很多地方也很好用。比如线程中定义线程的执行函数逻辑,智能指针中定制删除器等, lambda的应用还是很广泛的,以后我们会不断接触到。
🐱🏍lambda的原理
lambda的原理和范围for很像,编译后从汇编指令层的角度看,压根就没有lambda和范围for这样的东西。范围for底层是迭代器,而lambda底层是仿函数对象,也就说我们写了一个lambda以后,编译器会生成⼀个对应的仿函数的类。
仿函数的类名是编译按一定规则生成的,保证不同的lambda生成的类名不同;lambda参数/返回类型/函数体就是仿函数operator()的参数/返回类型/函数体,lambda的捕捉列表本质是生成的仿函数类的成员变量,也就是说捕捉列表的变量都是lambda这个仿函数的构造函数的实参。我们来看个例子:
当然,口说无凭,我们可以根据底层汇编代码来看看到底它们两个本质上是不是 一样的,底层汇编代码可不会骗人哦。
所以lambda的底层原理就是一个仿函数。 就上面而言,r2其实就是一个仿函数对象,r2(10000,2)就是调用仿函数中的operator()。
五、类的新功能
👶默认的移动构造和移动赋值
原来C++类中,有6个默认成员函数:构造函数/析构函数/拷贝构造函数/拷贝赋值重载/取地址重
载/const 取地址重载,最后重要的是前4个,后两个用处不大,默认成员函数就是我们不写编译器会默认生成。C++11新增了两个默认成员函数,移动构造函数和移动赋值运算符重载。
特别地,如果没有自己实现移动构造函数,且没有实现析构函数、拷贝构造、拷贝赋值重载,那么这时编译器会自动生成⼀个默认移动构造(这点与先前系统默认生成成员函数的条件有所不同)。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,对于自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用它的移动构造,没有实现就调用它的拷贝构造。
特别地,如果没有自己实现移动赋值重载函数,且没有实现析构函数、拷贝构造、拷贝赋值重载,那么这时编译器会自动生成⼀个默认移动赋值(这点与先前系统默认生成成员函数的条件有所不同)。默认生成的移动赋值函数,对于内置类型成员会执行逐成员按字节拷贝,对于自定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调用它的移动赋值,若没有实现就调用它的赋值重载。(默认移动赋值跟上面移动构造完全类似)
如果提供了移动构造或者移动赋值,编译器就不会自动生成拷移动构造和移动赋值了。
举个例子:
class Person
{
public:Person(const char* name = "张三", int age = 1):_name(name), _age(age){}
private:blue::string _name;int _age;
};int main()
{Person s1;Person s2 = std::move(s1);Person s3;s3 = std::move(s2);return 0;
}
此时,Person类中没有写移动构造和移动赋值,因为此时我们没有实现析构函数、拷贝构造、拷贝赋值重载中的任意一个,所以系统会默认生成移动构造和移动赋值。
我们来看运行结果:
结果正如我们上面所说的那样, 默认生成的移动构造/移动赋值,对于内置类型成员会执行逐成员按字节拷贝,对于自定义类型成员,则会调用它的移动构造/移动赋值。
若果我们写上一个析构函数:
class Person
{
public:Person(const char* name = "张三", int age = 1):_name(name), _age(age){}//添加析构函数~Person(){}private:blue::string _name;int _age;
};int main()
{Person s1;Person s2 = std::move(s1);Person s3;s3 = std::move(s2);return 0;
}
运行结果:
这时,系统就不会默认生成移动构造和移动赋值了。但会自动生成拷贝构造和赋值重载,默认生成的拷贝构造/赋值重载,对于内置类型成员会执行逐成员按字节拷贝,对于自定义类型成员,则会调用它的拷贝构造/赋值重载。
👶成员变量声明时给缺省值
成员变量声明时给缺省值是给初始化列表用的,如果没有显示在初始化列表初始化,就会在初始化列表用这个缺省值进行初始化。
例如:
👶defult和delete
C++11可以让你更好的控制要使用的默认函数。假设你要使用某个默认的函数,但是因为⼀些原因
这个函数没有默认生成。比如:我们提供了拷贝构造,就不会生成移动构造了,那么我们可以使用default关键字显示指定移动构造生成。注意:如果有移动构造,但没写拷贝构造,那么系统也不会默认生成拷贝构造了。
class Person
{
public:Person(const char* name = "张三", int age = 1):_name(name), _age(age){}~Person(){}//有析构函数了,系统就不会默认生成移动构造了Person(Person&& p) = default; //强制生成移动构造//如果有移动构造,但没写拷贝构造,那么系统也不会默认生成拷贝构造了(移动构造会影响拷贝构造)Person(const Person& p) = default; //强制生成拷贝构造private:blue::string _name;int _age;
};
在C++中,我们有时候不希望一个类可以被拷贝如(istream、ostream等),如果能想要限制某些默认函数的生成调用,那么在C++98中,是将该函数设置成private并且只声明不定义,这样只要其他人想要调用就会报错。在C++11中更简单,只需在该函数声明后加上=delete即可,该语法指示编译器不生成对应函数的默认版本,称=delete修饰的函数为删除函数。比如ostream就不允许拷贝:
func修改如下可以正常调用:
void func(ostream& out)
{}
👶 final与override
这两个关键字也是C++11新增的,我在这篇文章中有介绍 -> 【C++】多态
👶STL中一些变化
C++11中,STL中一些新的变化分为两个方面:
- 新容器
- 新接口
下图1圈起来的就是C++11中STL增加的新容器,但是实际最有用的容器就是unordered_map和unordered_set。这两个容器我在前面文章已经进行了非常详细的讲解,大家有兴趣可以去看看,其它的容器大家了解⼀下即可。
STL中容器也增加不少新接口,容器的push/insert/emplace系列接口都增加了与移动构造和移动赋值相关的接口,如果传的是右值,那么push/insert/emplace系列接口的效率就会提高,还有比如initializer_list版本的构造,以及范围for等。
initializer_list和范围for,它们并不会改变效率,只是书写形式上变得更简单了,它们的存在对于C++98来说是锦上添花。
其中STL最核心的变化有两点:
- unordered_map和unordered_set的出现
- emplace系列接口和push、insert右值引用版本的接口
这两点核心变化对于C++98来说就是雪中送炭。
六、包装器
🎈function
std::function是一个类模板,也是一个包装器。 用std::function实例化出来的对象可以包装存储其它的可调用对象,包括函数指针、仿函数、 lambda 、 bind表达式等,存储的可调用对象被称为std::function的目标,若std::function不含目标,则称它为空。function被定义在<functional>这个头文件中。
我们先来看一下function的用法:
#include<functional>//全局函数
int f(int a, int b)
{return a + b;
}//仿函数
struct Functor
{
public:int operator() (int a, int b){return a + b;}
};int main()
{//包装各种可调用对象function<int(int, int)> f1 = f; //int是返回值类型 (int ,int)是形参类型 包装函数指针function<int(int, int)> f2 = Functor(); //包装仿函数对象function<int(int, int)> f3 = [](int a, int b)->int { return a + b; }; //包装lambda对象cout << f1(1, 1) << endl;cout << f2(1, 1) << endl;cout << f3(1, 1) << endl;return 0;
}
运行结果:
通过包装器就可以将不同类型可调用对象统一起来。
除此之外,它还可以包装成员函数:
#include<functional>
class Plus
{
public:Plus(int n = 10):_n(n){}//静态成员函数static int plusi(int a, int b){return a + b;}//成员函数double plusd(double a, double b){return (a + b) * _n;}private:int _n;
};int main()
{//1、function<int(int, int)> f4 = &Plus::plusi; //包装静态成员函数,静态成员函数要指明类域(&可加可不加)cout << f4(1, 1) << endl;//2、function<double(Plus*, double, double)> f5 = &Plus::plusd; //包装成员函数,成员函数要指明类域并且前面加&才能获取地址Plus pl;cout << f5(&pl, 1.111, 1.1) << endl;//3、function<double(Plus, double, double)> f6 = &Plus::plusd;cout << f6(pl, 1.1, 1.1) << endl;cout << f6(Plus(), 1.1, 1.1) << endl;//4、function<double(Plus&&, double, double)> f7 = &Plus::plusd;cout << f7(move(pl), 1.1, 1.1) << endl;cout << f7(Plus(), 1.1, 1.1) << endl;return 0;
}
🎈bind
bind是一个函数模板,它也是⼀个可调用对象的包装器,可以把它看做是⼀个函数适配器,对接收的fn可调用对象进行处理后返回一个可调用对象。 bind可以用来调整参数个数和参数顺序,它也在<functional>这个头文件中。
调用bind的一般形式:auto newCallable = bind(callable,arg_list); 其中newCallable本身是一个可调用对象,arg_list是⼀个逗号分隔的参数列表,对应给定的callable的参数(callable是一个调用对象)。当我们调newCallable时,newCallable会调用callable,并传给它arg_list中的参数。
arg_list中的参数可能包含形如_n的名字,其中n是⼀个整数,这些参数是一个个占位符,表示callable的参数,它们占据了传递给callable的参数的位置。数值n表示生成的可调用对象中参数的位置:_1为callable的第⼀个参数,_2为第⼆个参数,以此类推。_1/_2/_3....这些占位符是放到placeholders的⼀个命名空间中。
我们来看一下它的用法:
1、调整参数顺序
#include <functional>
using placeholders::_1;
using placeholders::_2;
using placeholders::_3;int Sub(int a, int b)
{return (a - b) * 10;
}int main()
{auto sub1 = bind(Sub, _1, _2); //Sub是一个可调用对象(函数指针)cout << sub1(10, 5) << endl; //可以理解为:10对应_1即Sub第一个参数,5对应_2即Sub第二个参数//调整参数位置,_1始终代表第一个实参,_2始终代表第二个实参auto sub2 = bind(Sub, _2, _1);cout << sub2(10, 5) << endl; //可以理解为:10对应_1即Sub第二个参数,5对应_2即Sub第一个参数return 0;
}
运行结果:
在实际应用中,调整参数的用途不大。
2、调整参数个数(常用)
#include <functional>
using placeholders::_1;
using placeholders::_2;
using placeholders::_3;int Sub(int a, int b)
{return (a - b) * 10;
}int main()
{//调整参数个数(常用)auto sub3 = bind(Sub, 100, _1); //将参数a"绑死",a是固定不变的,始终是100cout << sub3(5) << endl;auto sub4 = bind(Sub, _1, 100); //将参数b"绑死",b是固定不变的,始终是100cout << sub4(5) << endl;return 0;
}
也可以这么玩:
int main()
{function<double(Plus&&, double, double)> f1 = &Plus::plusd;cout << f1(Plus(), 1.1, 1.1) << endl;//将成员函数对象进行绑死,就不需要每次都传递了function<double(double, double)> f2 = bind(&Plus::plusd, Plus(), _1, _2);cout << f2(1.1, 1.1) << endl;return 0;
}
bind返回的是一个可调用对象,function可以包装任何可调用对象,所以可以联合起来使用。
七、结语
本篇内容到这里就结束啦,希望对大家有帮助,C++11还有一个重要的东西,那就是智能指针,关于智能指针,我会专门写一篇文章讲述它,这里就不多说了🙊,最后祝各位生活愉快🙌!