【C++11】尽显锋芒

embedded/2024/11/26 11:00:10/

(续)

一、可变参数模板

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;
}

运行结果:

3f348cd0a1754ccca435893c22f64bb2.png

我简单来说一下这段代码的执行过程:

首先,执行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)这是一个函数模板,函数模板在确定类型时是在编译阶段完成的,所以,包展开的过程是在程序编译阶段完成的。

我们结合示意图来理解一下:

6ca91b190dca4822ac086cdc1c15b699.png

图中右边出现的函数统统在编译阶段由编译器实例化出来的!!!然后程序运行直接调用实例化出来的函数。  

有人会觉得上述逻辑有点麻烦,可以这样写:

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;
}

运行结果: 

542c531bb8224781b51c0b9b25b727de.png

上述写法就是我们自己写,程序运行直接调用即可;如果我们写的是模板,那么执行"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;
}

运行结果:

440d27b263c94ba9abc9fc631700c97d.png

这种方式不会有发生递归, 利用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接口):

1332c6f4662c4331a51512f2af2ed7a8.png

0804865768cd4de2ad0812f1ea9d9739.png

我们来看看这两个接口到底有什么不同的地方:

外部条件: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;
}

运行结果:

426992e61ebb4f25abc974f04b51afee.png

传左值,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;
}

运行结果:

919c287759254f348cc286f376d96e7a.png

传右值,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;
}

运行结果:

998d79863134466c9d3744ac8e945a79.png

这时,它们两个的调用结果就不一样了。

对于push_back来说:

3d5a9866b7284ba49d789bc860d1d8d1.png

在实例化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来说:

a0b72f47f20e48eda9841759f5dabeca.png

在实例化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;
}

运行结果:

116173b19e89486cb1cb33cafd9ecd34.png

传左值,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;
}

运行结果: 

0d05d6d11bfe407da34e6ea2a2c66db6.png

传右值,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;
}

运行结果:

d44a70bf11204c19a61ba4ed4b16d2a8.png

所以相较于插入来说,还是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;
}

运行结果:

656bc8a9c6e442288a8d57f2d133fb5d.png

是不是也没有那么复杂?我们再来写一个:

int main()
{//1、捕捉列表为空也不能省略//2、参数列表为空可以省略//3、返回值可以省略,可以通过返回对象自动推导//4、函数体不能省略auto func1 = []{cout << "hello bit" << endl;return 0;};func1();return 0;
}

运行结果:

4d8bff1bd0a34f5ba49f82753c6c1ff4.png

通过上面两个例子,相信大家已经了解了lambda的基本使用方法,接下来,我们来看一看捕捉列表到底有什么作用。

🐱‍🏍捕捉列表

lambda表达式中默认只能用lambda函数体和参数列表中的变量,如果想用外层作用域中的变量就
需要进行捕捉。

acc454f107434a928d25841cb47836e2.png

🐱‍👤捕捉方式一:

第一种捕捉方式是在捕捉列表中显示的传值捕捉和传引用捕捉,捕捉的多个变量用逗号分隔;比如:[x,y,&z],其中x和y是值捕捉,z是引用捕捉。通常情况下,引用都是和类型绑在一起的,&z大家第一眼可能认为是取地址,但在捕捉列表中&表示引用,这点是特殊的,大家不要记错。

f07de21594a644d6b284e1384a8e1716.png

使用值捕捉的变量不能被修改(默认被const修饰)。

如果将a、b定义到lambda表达式下面,会报错,因为编译器走到捕捉列表时,只会向上查找,不会向下查找。

01fbb86b103d4491acf6b43d476a46bb.png

特殊地,在类域中,若要使用某个变量会在整个类域中查找。

在lambda的函数体中可以直接使用全局域的东西,不需要通过捕捉列表捕捉,也不能捕捉:

17a21c20fb50449bb51dcf94725d1e74.png

注意:同一个变量不能捕捉两次。 

🐱‍👤捕捉方式二: 

第二种捕捉方式是在捕捉列表中隐式捕捉,我们在捕捉列表写一个=表示隐式值捕捉,在捕捉列表写⼀个&表示隐式引用捕捉,这样我们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表达式如果定义在全局位置,捕捉列表必须为空(因为没有东西能捕捉)。

eb636e946fb448c38bdcb1defceaed48.png

默认情况下,传值捕捉的过来的对象不能被修改(被const修饰),但将mutable加在参数列表的后面可以取消其常量性,也就说使用该修饰符后,传值捕捉的对象就可以修改了,但是修改还是针对形参对象,不会影响外面实参;使用该修饰符(mutable)后,参数列表不可省略(即使参数列表为空)。

ed6ee2907e43407c8ae9eb5c942b1178.png

🐱‍🏍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这个仿函数的构造函数的实参。我们来看个例子:

da6ef09e164044cfbd8a17c4b6ba6e9d.png

当然,口说无凭,我们可以根据底层汇编代码来看看到底它们两个本质上是不是 一样的,底层汇编代码可不会骗人哦。

4458dfd3ded549248712436f88a53586.png

786142d0a20b4ca7b3a317bfd09cb885.png

所以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类中没有写移动构造和移动赋值,因为此时我们没有实现析构函数、拷贝构造、拷贝赋值重载中的任意一个,所以系统会默认生成移动构造和移动赋值。

我们来看运行结果:

c108aef6b5f641eca484d4e284482831.png

结果正如我们上面所说的那样, 默认生成的移动构造/移动赋值,对于内置类型成员会执行逐成员按字节拷贝,对于自定义类型成员,则会调用它的移动构造/移动赋值。

若果我们写上一个析构函数:

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;
}

运行结果:

3954be9b820f4b40b006b7d1158aca28.png

这时,系统就不会默认生成移动构造和移动赋值了。但会自动生成拷贝构造和赋值重载,默认生成的拷贝构造/赋值重载,对于内置类型成员会执行逐成员按字节拷贝,对于自定义类型成员,则会调用它的拷贝构造/赋值重载。

👶成员变量声明时给缺省值

成员变量声明时给缺省值是给初始化列表用的,如果没有显示在初始化列表初始化,就会在初始化列表用这个缺省值进行初始化。

例如:

508f17d976ec4dd8b288ac467e331a51.png

👶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就不允许拷贝:

31ebbb0409cf435f880d9bef6149825f.png

50fbf287a4734c4faec28b1e4985e34c.png

func修改如下可以正常调用:

void func(ostream& out)  
{}

👶 final与override

这两个关键字也是C++11新增的,我在这篇文章中有介绍 -> 【C++】多态

👶STL中一些变化

C++11中,STL中一些新的变化分为两个方面:

  • 新容器
  • 新接口

下图1圈起来的就是C++11中STL增加的新容器,但是实际最有用的容器就是unordered_map和unordered_set。这两个容器我在前面文章已经进行了非常详细的讲解,大家有兴趣可以去看看,其它的容器大家了解⼀下即可。

1c93505d8eb44838a63b3c856eac6b92.png

STL中容器也增加不少新接口,容器的push/insert/emplace系列接口都增加了与移动构造和移动赋值相关的接口,如果传的是右值,那么push/insert/emplace系列接口的效率就会提高,还有比如initializer_list版本的构造,以及范围for等。

initializer_list和范围for,它们并不会改变效率,只是书写形式上变得更简单了,它们的存在对于C++98来说是锦上添花

其中STL最核心的变化有两点:

  1. unordered_map和unordered_set的出现
  2. emplace系列接口和push、insert右值引用版本的接口

这两点核心变化对于C++98来说就是雪中送炭

六、包装器

🎈function

97569f8598a34e24ba17661107db6742.png

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;
}

运行结果:

71966144314f445c942508fd3dcc6031.png

通过包装器就可以将不同类型可调用对象统一起来。 

除此之外,它还可以包装成员函数:

#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;
}

307817123f374a8b897d4e56950ae3d0.png

🎈bind

28183b4c53724b1a99ed1b983871c584.png

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的⼀个命名空间中。

37ee67f4a7ca4cc6b185bd0c5dde9001.png

我们来看一下它的用法:

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;
}

运行结果:

05e396db5bd84214855cf9b3d3847133.png

e94202aac0714cf9804f699bf53fc268.png

在实际应用中,调整参数的用途不大。 

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还有一个重要的东西,那就是智能指针,关于智能指针,我会专门写一篇文章讲述它,这里就不多说了🙊,最后祝各位生活愉快🙌!

 


http://www.ppmy.cn/embedded/140599.html

相关文章

HTML5和CSS3新增特性

HTML5的新特性 HTML5新增的语义化标签 HTML5 的新增特性主要是针对于以前的不足&#xff0c;增加了一些新的标签、新的表单和新的表单属性等。 这些新特性都有兼容性问题&#xff0c;基本是 IE9 以上版本的浏览器才支持&#xff0c;如果不考虑兼容性问题&#xff0c;可以大量…

MySQL中的锁与优化SQL查询性能

MySQL作为一种高效、稳定、易用的开源关系型数据库管理系统&#xff08;RDBMS&#xff09;&#xff0c;在大数据量和高并发的场景中&#xff0c;其性能优化显得尤为重要。锁机制和SQL查询优化是MySQL性能调优的两个关键方面。本文将详细探讨MySQL中的锁类型以及如何优化SQL查询…

【VUE3】VUE组合式(响应式)API常见语法

ref() //const count ref(0) //count.value&#xff08;访问值&#xff0c;包括对象要加.value&#xff09; //任何类型的值&#xff0c;包括深层嵌套的对象或则JS内置数据结构 await nextTick() //要等待 DOM 更新完成后再执行额外的代码&#xff0c;可以使用 nextTick() …

(免费送源码)计算机毕业设计原创定制:Java+SSM+JSP+Ajax+MySQLSSM国外鞋服代购平台

摘 要 随着科学技术的飞速发展&#xff0c;社会的方方面面、各行各业都在努力与现代的先进技术接轨&#xff0c;通过科技手段来提高自身的优势&#xff0c;鞋服代购平台当然也不例外。代购平台是以实际运用为开发背景&#xff0c;运用软件工程原理和开发方法&#xff0c;采用…

ODBC连接PostgreSQL数据库后,网卡DOWN后,客户端进程阻塞问题解决方法

问题现象&#xff1a;数据库客户端进程数据库连接成功后&#xff0c;再把跟数据库交互的网卡down掉&#xff0c;客户端进程就会阻塞&#xff0c;无法进行其他处理。该问题跟TCP keepalive机制有关。 可以在odbc.ini文件中增加相应的属性来解决&#xff0c;在odbc.ini 增加如下…

STM32 外设简介

STM32 外设简介 STM32 是由意法半导体 (STMicroelectronics) 开发的一系列基于 ARM Cortex 内核的微控制器&#xff0c;广泛应用于嵌入式系统中。STM32 系列的一个重要特点是其丰富而强大的外设模块&#xff0c;支持多种接口和功能&#xff0c;能满足工业控制、物联网、消费电…

认识RabbitMq和RabbitMq的使用

1 认识RabbitMq RabbitMQ是⼀个消息中间件&#xff0c;也是⼀个生产者消费者模型&#xff0c;它负责接收&#xff0c;存储并转发消息。 2.1 Producer和Consumer Producer&#xff1a;生产者&#xff0c;是RabbitMQServer的客户端&#xff0c;向RabbitMQ发送消息 Consumer&…

affine_grid转onnx issue记录

affine_grid的官方链接: torch.nn.functional.affine_grid — PyTorch 2.5 documentation [ONNX] Support affine_grid_generator Issue #30563 pytorch/pytorch GitHub import torch import torch.nn as nnclass Model(nn.Module):def __init__(self):super(Model, sel…