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

C++ —— 剑斩旧我 破茧成蝶—C++11

江河入海,知识涌动,这是我参与江海计划的第2篇。

目录

1. C++11的发展历史

2. 列表初始化

2.1 C++98传统的{}

2.2 C++11中的{}

2.3 C++11中的std::initializer_list

3. 右值引用和移动语义

3.1 左值和右值

3.2 左值引用和右值引用

3.3 引用延长生命周期

3.4 左值和右值的参数匹配

3.5 右值引用和移动语义的使用场景

3.5.1 左值引用主要使用场景回顾

3.5.2 移动构造和移动赋值

3.5.3 右值引用和移动语义解决传值返回问题

情况1. 右值对象构造,只有拷贝构造,没有移动构造的场景

情况2. 右值对象构造,有拷贝构造,也有移动构造的场景

情况3. 右值对象赋值,只有拷贝构造和拷贝赋值,没有移动构造和移动赋值的场景

情况4. 右值对象赋值,既有拷贝构造和拷贝赋值,也有移动构造和移动赋值的场景

 3.5.4 右值引用和移动语义在传参中的提效

3.6 类型分类

3.7 引用折叠

3.8 完美转发

4. 可变参数模板

4.1 基本语法及原理

4.2 包扩展(解析出参数包的内容)

4.3 empalce系列接口

5. 新的类功能

5.1 默认的移动构造和移动赋值

5.2 声明时给缺省值

5.3 defult和delete

5.4 final与override 

6. STL中⼀些变化

7. lambda

7.1 lambda表达式语法

7.2 捕捉列表

7.3 lambda的应用

7.4 lambda的原理

8. 包装器

8.1 function(包装)

8.2 bind (绑定)


C++参考手册:

cppreference.comicon-default.png?t=O83Ahttps://en.cppreference.com/w/


1. C++11的发展历史

C++11 是 C++ 的第⼆个主要版本,并且是从 C++98 起的最重要更新。它引⼊了⼤量更改,标准化了既有实践,并改进了对 C++ 程序员可⽤的抽象。在它最终由 ISO 在 2011 年 8 ⽉ 12 ⽇采纳前,⼈们曾使⽤名称“C++0x”,因为它曾被期待在 2010 年之前发布。C++03 与 C++11 期间花了 8 年时间,故⽽这是迄今为⽌最⻓的版本间隔。从那时起,C++ 有规律地每 3 年更新⼀次


2. 列表初始化

注意:列表初始化和初始化列表并不是同一个东西



2.1 C++98传统的{}

C++98中的数组和结构体可以用{}进⾏初始化

struct Point
{int _x;int _y;
};
int main()
{int array1[] = { 1, 2, 3, 4, 5 };int array2[5] = { 0 };Point p = { 1, 2 };return 0;
}

2.2 C++11中的{}

1. C++11以后想统⼀初始化⽅式,试图实现⼀切对象皆可⽤{}初始化,{}初始化也叫做列表初始化

    
2. 内置类型⽀持,⾃定义类型也⽀持,⾃定义类型本质是类型转换,中间会产⽣临时对象,最后优化了以后变成直接构造

    

// C++11支持的
// 内置类型支持
int x1 = { 2 };
int x2 = 2;// 自定义类型支持
// 这里本质是用{ 2025, 1, 1}构造一个Date临时对象
// 临时对象再去拷贝构造d1,编译器优化后合二为一变成{ 2025, 1, 1}直接构造初始化d1
// 运行一下,我们可以验证上面的理论,发现是没调用拷贝构造的
Date d1 = { 2025, 1, 1 };
Date d20(2025, 1, 1);// 这里d2引用的是{ 2024, 7, 25 }构造的临时对象,临时对象具有常性,所以要加上const
const Date& d2 = { 2024, 7, 25 };

 3. {}初始化的过程中,可以省略掉=(赋值符号)

   

	// 可以省略掉=Point p1{ 1, 2 };int x3{ 2 };Date d6{ 2024, 7, 25 };const Date& d7{ 2024, 7, 25 };// 不支持,只有使用{}初始化,才能省略=// Date d8 2025;

4. C++11列表初始化的本意是想实现⼀个⼤统⼀的初始化⽅式,其次他在有些场景下带来的不少便利,如容器push/inset多参数构造的对象时,{}初始化会很⽅便 

  

	v.push_back(d1);v.push_back(Date(2025, 1, 1));// 比起有名对象和匿名对象传参,这里{}更有性价比v.push_back({ 2025, 1, 1 });map<string, string> dict;dict.insert({ "xxx", "yyyy" });


2.3 C++11中的std::initializer_list

文档链接:

initializer_list - C++ Referenceicon-default.png?t=O83Ahttps://legacy.cplusplus.com/reference/initializer_list/initializer_list/

Initializer_list是c++11新增加的一个类型,和数组类似,它主要用来初始化

    

std::initializer_list一般是作为构造函数的参数

1. 上⾯的初始化已经很⽅便,但是对象容器初始化还是不太⽅便,⽐如⼀个vector对象,我想⽤N个值去构造初始化,那么我们得实现很多个构造函数才能⽀持, vector<int> v1 =
{1,2,3};vector<int> v2 = {1,2,3,4,5};

   
2. C++11库中提出了⼀个std::initializer_list的类

   

auto il = { 10, 20, 30 };

thetype of il is an initializer_list ,这个类的本质是底层开⼀个数组,将数据拷⻉过来

    

std::initializer_list内部两个指针分别指向数组的开始结束

   

3. std::initializer_list⽀持迭代器遍历

   

4. 容器⽀持⼀个std::initializer_list的构造函数,也就⽀持任意多个值构成的 {x1,x2,x3...} 进⾏初始化。STL中的容器⽀持任意多个值构成的 {x1,x2,x3...} 进⾏初始化,就是通过
std::initializer_list的构造函数⽀持的

int main()
{std::initializer_list<int> mylist;mylist = { 10, 20, 30 };cout << sizeof(mylist) << endl;// 这⾥begin和end返回的值initializer_list对象中存的两个指针// 这两个指针的值跟i的地址跟接近,说明数组存在栈上int i = 0;cout << mylist.begin() << endl;cout << mylist.end() << endl;cout << &i << endl;// {}列表中可以有任意多个值// 这两个写法语义上还是有差别的,第⼀个v1是直接构造,// 第⼆个v2是构造临时对象+临时对象拷⻉v2+优化为直接构造vector<int> v1({ 1,2,3,4,5 });vector<int> v2 = { 1,2,3,4,5 };const vector<int>& v3 = { 1,2,3,4,5 };// 这⾥是pair对象的{}初始化和map的initializer_list构造结合到⼀起⽤了map<string, string> dict = { {"sort", "排序"}, {"string", "字符串"} };// initializer_list版本的赋值⽀持v1 = { 10,20,30,40,50 };return 0;
}


3. 右值引用和移动语义

C++98的C++语法中就有引⽤的语法,⽽C++11中新增了的右值引⽤语法特性,我们之前学习的引⽤就叫做左值引⽤。⽆论左值引⽤还是右值引⽤,都是给对象取别名

3.1 左值和右值

1. 左值是⼀个表⽰数据的表达式(如变量名或解引⽤的指针),⼀般是有持久状态,存储在内存中,我们可以获取它的地址,左值可以出现赋值符号的左边,也可以出现在赋值符号右边

    

定义时const修饰符后的左值,不能给他赋值,但是可以取它的地址

  

// 左值:可以取地址
// 以下的p、b、c、*p、s、s[0]就是常⻅的左值
int* p = new int(0);
int b = 1;
const int c = b;
*p = 10;
string s("111111");
s[0] = 'x';cout << &c << endl;
cout << (void*)&s[0] << endl;

   
2. 右值也是⼀个表⽰数据的表达式要么是字⾯值常量、要么是表达式求值过程中创建的临时对象,右值可以出现在赋值符号的右边,但是不能出现在赋值符号的左边,右值不能取地址

   

// 右值:不能取地址
double x = 1.1, y = 2.2;
// 以下⼏个10、x + y、fmin(x, y)、string("11111")都是常⻅的右值
10;
x + y;
fmin(x, y);
string("11111");//右值是不能取地址的,不然就会报错,下面的代码就是
//cout << &10 << endl;
//cout << &(x+y) << endl;
//cout << &(fmin(x, y)) << endl;
//cout << &string("11111") << endl;

  


3. 值得⼀提的是,左值的英⽂简写为lvalue,右值的英⽂简写为rvalue。传统认为它们分别是leftvalue、right value 的缩写

   

现代C++中,lvalue 被解释为loactor value的缩写,可意为存储在内存中、有明确存储地址可以取地址的对象,⽽ rvalue 被解释为 read value,指的是那些可以提供数据值,但是不可以寻址

   

例如:临时变量,字⾯量常量,存储于寄存器中的变量等,也就是说左值和右值的核⼼区别就是能否取地址


3.2 左值引用和右值引用

1. Type& r1 = x; Type&& rr1 = y; 第⼀个语句就是左值引⽤左值引⽤就是给左值取别名,第⼆个就是右值引⽤右值引⽤就是给右值取别名

  

// 左值引⽤给左值取别名
int& r1 = b;
int*& r2 = p;
int& r3 = *p;
string& r4 = s;
char& r5 = s[0];// 右值引⽤给右值取别名
int&& rr1 = 10;
double&& rr2 = x + y;
double&& rr3 = fmin(x, y);
string&& rr4 = string("11111");

   
2. 左值引⽤不能直接引⽤右值,但是const左值引⽤可以引⽤右值

   
3. 右值引⽤不能直接引⽤左值,但是右值引⽤可以引⽤move(左值)

  

// 左值引⽤不能直接引⽤右值,但是const左值引⽤可以引⽤右值
const int& rx1 = 10;
const double& rx2 = x + y;
const double& rx3 = fmin(x, y);
const string& rx4 = string("11111");// 右值引⽤不能直接引⽤左值,但是右值引⽤可以引⽤move(左值)//int&& rrx1 = b;这样直接引用是不行的int&& rrx1 = move(b);
int*&& rrx2 = move(p);
int&& rrx3 = move(*p);
string&& rrx4 = move(s);
string&& rrx5 = (string&&)s;

   
4.

template <class T> typename remove_reference<T>::type&& move (T&&arg);

move是库⾥⾯的⼀个函数模板,本质内部是进⾏强制类型转换,move不会改变对象的本身的属性,只是中间产生了一个临时对象进行强制类型转换,被强制类型转换的值到下一行还是左值

    
5. 需要注意的是变量表达式都是左值属性,也就意味着⼀个右值被右值引⽤绑定后,右值引⽤变量变量表达式的属性是左值

   

                               左值引用的属性是左值,右值引用的属性也是左值                                    

// b、r1、rr1都是变量表达式,都是左值
cout << &b << endl;
cout << &r1 << endl;
cout << &rr1 << endl;// 这⾥要注意的是,rr1的属性是左值,所以不能再被右值引⽤绑定,除⾮move⼀下
int& r6 = rr1;
// int&& rrx6 = rr1;
int&& rrx6 = move(rr1);

  


6. 语法层⾯看,左值引⽤和右值引⽤都是取别名,不开空间。从汇编底层的⻆度看下⾯代码中r1和rr1汇编层实现,底层都是⽤指针实现的,没什么区别


3.3 引用延长生命周期

右值引⽤可⽤于为临时对象和匿名对象延⻓⽣命周期,const 的左值引⽤也能延⻓临时对象⽣存期,但这些对象⽆法被修改

  

临时对象的⽣命周期只在当前那一行

int main()
{// std::string&& r1 = s1;// 错误:不能绑定到左值std::string s1 = "Test";// OK:到 const 的左值引⽤延⻓⽣存期// r2 += "Test";// 错误:不能通过到 const 的引⽤修改const std::string& r2 = s1 + s1;std::string&& r3 = s1 + s1;// OK:右值引⽤延⻓⽣存期r3 += "Test";// OK:能通过到⾮ const 的引⽤修改std::cout << r3 << '\n';return 0;
}


3.4 左值和右值的参数匹配

1. C++98中,我们实现⼀个const左值引⽤作为参数的函数,那么实参传递左值和右值都可以匹配

   
2. C++11以后,分别重载左值引⽤、const左值引⽤、右值引⽤作为形参的f函数那么实参是左值会匹配f(左值引⽤),实参是const左值会匹配f(const 左值引⽤),实参是右值会匹配f(右值引⽤),同时具有三个会构成函数重载,编译器会去调用最匹配的

  

void f(int& x)
{std::cout << "左值引⽤重载 f(" << x << ")\n";
}
void f(const int& x)
{std::cout << "到 const 的左值引⽤重载 f(" << x << ")\n";
}
void f(int&& x)
{std::cout << "右值引⽤重载 f(" << x << ")\n";
}
int main()
{int i = 1;const int ci = 2;f(i); // 调⽤ f(int&)f(ci); // 调⽤ f(const int&)f(3); // 调⽤ f(int&&),如果没有 f(int&&) 重载则会调⽤ f(const int&)f(std::move(i)); // 调⽤ f(int&&)}

   
3. 右值引⽤变量在⽤于表达式时属性是左值

  

之所以这样设计是因为右值本身不能被修改,但是右值被右值引⽤之后本身的属性变成了左值之后就可以修改了,因为只有左值才能修改

 

// 右值引⽤变量在⽤于表达式时是左值int&& x = 1;f(x);// 调⽤ f(int& x)f(std::move(x)); // 调⽤ f(int&& x)return 0;

 

                               左值引用的属性是左值,右值引用的属性也是左值                                    


3.5 右值引用和移动语义的使用场景


3.5.1 左值引用主要使用场景回顾

左值引⽤主要使⽤场景是在函数中:

  

                                                        1. 左值引⽤传参

  

                                                        2. 左值引⽤传返回值时减少拷⻉

  

                                                        3. 修改实参和修改返回对象的价值

     

左值引⽤已经解决⼤多数场景的拷⻉效率问题,但是有些场景不能使⽤传左值引⽤返回,如addStrings和generate函数(两个字符串相加),C++98中的解决⽅案只能是被迫使⽤输出型参数解决

    

左值引用本质是返回对象的别名,但是这两个函数里是局部对象,返回之后就会销毁,相当于是野指针

  

return str;return vv;

  

那么C++11以后这⾥可以使⽤右值引⽤做返回值解决吗?显然是不可能的,因为这⾥的本质是返回对象是⼀个局部对象函数结束这个对象就析构销毁了,右值引⽤返回也⽆法概念对象已经析构销毁的事实 

 

                              左值引⽤和右值引⽤的最终目的都是减少拷贝,提高效率                          

  


3.5.2 移动构造和移动赋值

1. 移动构造函数是⼀种构造函数类似拷⻉构造函数,移动构造函数要求第⼀个参数是该类类型的引⽤,但是不同的是要求这个参数是右值引⽤,如果还有其他参数,额外的参数必须有缺省值

    
2. 移动赋值是⼀个赋值运算符的重载,他跟拷⻉赋值构成函数重载,类似拷⻉赋值函数,移动赋值函数要求第⼀个参数是该类类型的引⽤,但是不同的是要求这个参数是右值引⽤

    
3. 对于像string/vector这样的深拷⻉的类或者包含深拷⻉的成员变量的类,移动构造和移动赋值才有意义,因为移动构造和移动赋值的第⼀个参数都是右值引⽤的类型,他的本质是要“掠夺(交换)”引⽤的右值对象的资源,⽽不是像拷⻉构造和拷⻉赋值那样去拷⻉资源,从提⾼效率

   

  

下⾯的string样例实现了移动构造和移动赋值,我们需要结合场景理解 

namespace bit
{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){::swap(_str, ss._str);::swap(_size, ss._size);::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()
{bit::string s1("xxxxx");// 拷⻉构造bit::string s2 = s1;// 构造+移动构造,优化后直接构造bit::string s3 = bit::string("yyyyy");// 移动构造bit::string s4 = move(s1);cout << "******************************" << endl;return 0;
}

3.5.3 右值引用和移动语义解决传值返回问题

namespace bit
{string addStrings(string num1, string num2){string str;int end1 = num1.size() - 1, end2 = num2.size() - 1;int next = 0;while (end1 >= 0 || end2 >= 0){int val1 = end1 >= 0 ? num1[end1--] - '0' : 0;int val2 = end2 >= 0 ? num2[end2--] - '0' : 0;int ret = val1 + val2 + next;next = ret / 10;ret = ret % 10;str += ('0' + ret);}if (next == 1)str += '1';reverse(str.begin(), str.end());cout << "******************************" << endl;return str;}
}
// 场景1
int main()
{bit::string ret = bit::addStrings("11111", "2222");cout << ret.c_str() << endl;return 0;
}
// 场景2
int main()
{bit::string ret;ret = bit::addStrings("11111", "2222");cout << ret.c_str() << endl;return 0;
}


情况1. 右值对象构造,只有拷贝构造,没有移动构造的场景

1. 图1展⽰了vs2019 debug环境下编译器对拷⻉的优化,左边为不优化的情况下,两次拷⻉构造,右边为编译器优化的场景下连续步骤中的拷⻉合⼆为⼀变为⼀次拷⻉构造

   
2. 需要注意的是在vs2019的release和vs2022的debug和release,下⾯代码优化为⾮常恐怖,会直接将str对象的构造,str拷⻉构造临时对象,临时对象拷⻉构造ret对象,合三为⼀,变为直接构造。变为直接构造。要理解这个优化要结合局部对象⽣命周期和栈帧的⻆度理解,如图3所⽰

图1 


情况2. 右值对象构造,有拷贝构造,也有移动构造的场景

1. 图2展⽰了vs2019 debug环境下编译器对拷⻉的优化,左边为不优化的情况下,两次移动构造,右边为编译器优化的场景下连续步骤中的拷⻉合⼆为⼀变为⼀次移动构造

    
2. 需要注意的是在vs2019的release和vs2022的debug和release,下⾯代码优化为⾮常恐怖,会直接将str对象的构造,str拷⻉构造临时对象,临时对象拷⻉构造ret对象,合三为⼀,变为直接构造。要理解这个优化要结合局部对象⽣命周期和栈帧的⻆度理解,如图3所⽰

图2 

图3


情况3. 右值对象赋值,只有拷贝构造和拷贝赋值,没有移动构造和移动赋值的场景

1. 图4左边展⽰了vs2019 debug和 g++ test.cpp -fno-elide-constructors 关闭优化环境
下编译器的处理,⼀次拷⻉构造,⼀次拷⻉赋值

   
2. 需要注意的是在vs2019的release和vs2022的debug和release,下⾯代码会进⼀步优化,直接构造要返回的临时对象,str本质是临时对象的引⽤,底层⻆度⽤指针实现。运⾏结果的⻆度,我们可以看到str的析构是在赋值以后,说明str就是临时对象的别名

图4


情况4. 右值对象赋值,既有拷贝构造和拷贝赋值,也有移动构造和移动赋值的场景

1. 图5左边展⽰了vs2019 debug和 g++ test.cpp -fno-elide-constructors 关闭优化环境
下编译器的处理,⼀次移动构造,⼀次移动赋值

   
2. 需要注意的是在vs2019的release和vs2022的debug和release,下⾯代码会进⼀步优化,直接构造要返回的临时对象,str本质是临时对象的引⽤,底层⻆度⽤指针实现。运⾏结果的⻆度,我们可以看到str的析构是在赋值以后,说明str就是临时对象的别名

图5 


 3.5.4 右值引用和移动语义在传参中的提效

1. 查看STL⽂档我们发现C++11以后容器的push和insert系列的接⼝否增加的右值引⽤版本

   
2. 当实参是⼀个左值时,容器内部继续调⽤拷⻉构造进⾏拷⻉,将对象拷⻉到容器空间中的对象

   
3. 当实参是⼀个右值,容器内部则调⽤移动构造,右值对象的资源到容器空间的对象上


3.6 类型分类

 cppreference.com 和 Value categories这两个关于值类型的中⽂和英⽂的官⽅⽂档

  

值类别 - cppreference.comicon-default.png?t=O83Ahttps://zh.cppreference.com/w/cpp/language/value_category

值类别 - cppreference.comicon-default.png?t=O83Ahttps://zh.cppreference.com/w/cpp/language/value_category

1. C++11以后,进⼀步对类型进⾏了划分,右值被划分纯右值(pure value,简称prvalue)和将亡值(expiring value,简称xvalue)

   
2. 纯右值是指那些字⾯值常量或求值结果相当于字⾯值或是⼀个不具名的临时对象

   

如: 42、true、nullptr 或者类似 str.substr(1, 2)、str1 + str2 传值返回函数调⽤,或者整形 a、b,a++,a+b 等

     

纯右值和将亡值C++11中提出的,C++11中的纯右值概念划分等价于C++98中的右值

   
3. 将亡值是指返回右值引⽤的函数的调⽤表达式和转换为右值引⽤的转换函数的调⽤表达

   

如move(x)、static_cast<X&&>(x)

    
4. 泛左值(generalized value,简称glvalue),泛左值包含将亡值和左值

5. 有名字,就是glvalue;有名字,且不能被move,就是lvalue;有名字,且可以被move,就是xvalu;没有名字,且可以被移动,则是prvalue

 


3.7 引用折叠

1. C++中不能直接定义引⽤的引⽤如 int& && r = i; ,这样写会直接报错,但是可以通过模板或 typedef中的类型操作可以构成引⽤的引⽤

   

引⽤的引⽤会形成一个东西叫做引用折叠

  

typedef int& lref;
typedef int&& rref;
int n = 0;lref& r1 = n; // r1 的类型是 int&
lref&& r2 = n; // r2 的类型是 int&
rref& r3 = n; // r3 的类型是 int&
rref&& r4 = 1; // r4 的类型是 int&&

2. 通过模板或 typedef 中的类型操作可以构成引⽤的引⽤时,这时C++11给出了⼀个引⽤折叠的规则:只有右值引⽤的右值引⽤折叠成右值引⽤,所有其他组合均折叠成左值引⽤

// 由于引⽤折叠限定,f1实例化以后总是⼀个左值引⽤
template<class T>
void f1(T& x)
{}
// 由于引⽤折叠限定,f2实例化后可以是左值引⽤,也可以是右值引⽤
template<class T>
void f2(T&& x)
{}
int main()
{typedef int& lref;typedef int&& rref;int n = 0;lref& r1 = n; // r1 的类型是 int&lref&& r2 = n; // r2 的类型是 int&rref& r3 = n; // r3 的类型是 int&rref&& r4 = 1; // r4 的类型是 int&&// 没有折叠->实例化为void f1(int& x)f1<int>(n);f1<int>(0); // 报错// 折叠->实例化为void f1(int& x)f1<int&>(n);f1<int&>(0); // 报错// 折叠->实例化为void f1(int& x)f1<int&&>(n);f1<int&&>(0); // 报错// 折叠->实例化为void f1(const int& x)f1<const int&>(n);f1<const int&>(0);// 折叠->实例化为void f1(const int& x)f1<const int&&>(n);f1<const int&&>(0);// 没有折叠->实例化为void f2(int&& x)f2<int>(n);// 报错f2<int>(0);// 折叠->实例化为void f2(int& x)f2<int&>(n);f2<int&>(0); // 报错// 折叠->实例化为void f2(int&& x)f2<int&&>(n); // 报错f2<int&&>(0);return 0;
}

3. 像f2这样的函数模板中,T&& x参数看起来是右值引⽤参数,但是由于引⽤折叠的规则,他传递左值时就是左值引⽤,传递右值时就是右值引⽤,有些地⽅也把这种函数模板的参数叫做万能引⽤

   
4. Function(T&& t)函数模板程序中,假设实参是int右值,模板参数T的推导int,实参是int左值,模板参数T的推导int&再结合引⽤折叠规则,就实现了实参是左值,实例化出左值引⽤版本形参的Function,实参是右值,实例化出右值引⽤版本形参的Function

 

template<class T>
void Function(T&& t)
{int a = 0;T x = a;//x++;cout << &a << endl;cout << &x << endl << endl;
}
int main()
{// 10是右值,推导出T为int,模板实例化为void Function(int&& t)Function(10);// 右值int a;// a是左值,推导出T为int&,引⽤折叠,模板实例化为void Function(int& t)Function(a);// 左值// std::move(a)是右值,推导出T为int,模板实例化为void Function(int&& t)Function(std::move(a));// 右值const int b = 8;// a是左值,推导出T为const int&,引⽤折叠,模板实例化为void Function(const int&// 所以Function内部会编译报错,x不能++Function(b);// const 左值// std::move(b)右值,推导出T为const int,模板实例化为void Function(const int&&t)// 所以Function内部会编译报错,x不能++Function(std::move(b));// const 右值return 0;
}

3.8 完美转发

1. Function(T&& t)函数模板程序中,传左值实例化以后是左值引⽤的Function函数,传右值实例化以后是右值引⽤的Function函数

   
2. 结合5.2章节来看,变量表达式都是左值属性,也就意味着⼀个右值被右值引⽤绑定后,右值引⽤变量表达式的属性是左值,也就是说Function函数中t的属性是左值,那么我们把t传递给下⼀层函数Fun,那么匹配的都是左值引⽤版本的Fun函数。这⾥我们想要保持t对象的属性,就需要使⽤完美转发实现

   
3. template <class T> T&& forward (typename remove_reference<T>::type&arg);

   
4. template <class T> T&& forward (typenameremove_reference<T>::type&& arg);

   
5. 完美转发forward本质是⼀个函数模板,他主要还是通过引⽤折叠的⽅式实现,下⾯⽰例中传递给Function的实参是右值,T被推导为int,没有折叠,forward内部t被强转为右值引⽤返回;传递给Function的实参是左值,T被推导为int&,引⽤折叠为左值引⽤,forward内部t被强转为左值引⽤返回

template <class _Ty>
_Ty&& forward(remove_reference_t<_Ty>& _Arg) noexcept
{// forward an lvalue as either an lvalue or an rvaluereturn static_cast<_Ty&&>(_Arg);
}
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; }template<class T>int main()
{// 10是右值,推导出T为int,模板实例化为void Function(int&& t)Function(10);// 右值int a;// a是左值,推导出T为int&,引⽤折叠,模板实例化为void Function(int& t)Function(a);// 左值// std::move(a)是右值,推导出T为int,模板实例化为void Function(int&& t)Function(std::move(a));// 右值const int b = 8;// a是左值,推导出T为const int&,引⽤折叠,模板实例化为void Function(const int&// 所以Function内部会编译报错,x不能++Function(b);// const 左值// std::move(b)右值,推导出T为const int,模板实例化为void Function(const int&&t)// 所以Function内部会编译报错,x不能++Function(std::move(b));// const 右值return 0;
}

4. 可变参数模板


4.1 基本语法及原理

1. C++11⽀持可变参数模板,也就是说⽀持可变数量参数的函数模板和类模板,可变数⽬的参数被称为参数包,存在两种参数包:

                                                                1. 模板参数包,表⽰零或多个模板参数

  

                                                                2. 函数参数包:表⽰零或多个函数参数

  

//Args:类型名称,随便写
//<class ...Args>:模板参数包
//(Args... args):函数参数包template <class ...Args> void Func(Args... args) {}//传值传参template <class ...Args> void Func(Args&... args) {}//左值引用template <class ...Args> void Func(Args&&... args) {}//万能引用


2. 我们省略号来指出⼀个模板参数或函数参数的表⽰⼀个包

   

在模板参数列表中,class...或typename...指出接下来的参数表⽰零或多个类型列表

   

在函数参数列表中,类型名后⾯跟...指出接下来表⽰零或多个形参对象列表

    

函数参数包可以⽤左值引⽤或右值引⽤表⽰,跟前⾯普通模板⼀样,每个参数实例化时遵循引⽤折叠规则(看起来是右值引用,实际上是万能引用)


3. 可变参数模板的原理跟模板类似,本质还是去实例化对应类型和个数的多个函数

   
4. 这⾥我们可以使⽤sizeof...运算符去计算参数包中参数的个数

template <class ...Args>
void Print(Args&&... args)
{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;
}

 原理1:编译本质这⾥会结合引⽤折叠规则实例化出以下四个函数

void Print();
void Print(int&& arg1);
void Print(int&& arg1, string&& arg2);
void Print(double&& arg1, string&& arg2, double& arg3);

原理2:更本质去看没有可变参数模板,我们实现出这样的多个函数模板才能⽀持这⾥的功能,有了可变参数模板,我们进⼀步被解放,他是类型泛化基础上叠加数量变化,让我们泛型编程更灵活 

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


4.2 包扩展(解析出参数包的内容)

1. 对于⼀个参数包,我们除了能计算他的参数个数,我们能做的唯⼀的事情就是扩展它,当扩展⼀个包时,我们还要提供⽤于每个扩展元素的模式

    

扩展⼀个包就是将它分解为构成的元素,对每个元素应⽤模式,获得扩展后的列表。我们通过在模式的右边放⼀个省略号(...)来触发扩展操作

   
2. C++还⽀持更复杂的包扩展,直接将参数包依次展开依次作为实参给⼀个函数去处理

底层的实现细节:

包扩展的方式1: 

//包扩展的方式void ShowList()
{// 编译器时递归的终⽌条件,参数包是0个时,直接匹配这个函数cout << endl;
}template <class T, class ...Args>void ShowList(T x, Args... args)
{cout << x << " ";// args是N个参数的参数包// 调⽤ShowList,参数包的第⼀个传给x,剩下N-1传给第⼆个参数包ShowList(args...);
}// 编译时递归推导解析参数
template <class ...Args>
void Print(Args... args)
{ShowList(args...);
}int main()
{Print();Print(1);Print(1, string("xxxxx"));Print(1, string("xxxxx"), 2.2);return 0;
}

包扩展的方式2: 

template <class T>
const T& GetArg(const T& x)
{cout << x << " ";return x;
}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;
}


4.3 empalce系列接口

template <class... Args> void emplace_back (Args&&... args);template <class... Args> iterator emplace (const_iterator position,
Args&&... args);

1. C++11以后STL容器新增了empalce系列的接⼝,empalce系列的接⼝均为模板可变参数,功能上兼容push和insert系列

   

但是empalce还⽀持新玩法,假设容器为container<T>,empalce还⽀持直接插⼊构造T对象的参数,这样有些场景会更⾼效⼀些,可以直接在容器空间上构造T对象

    
2. emplace_back总体⽽⾔是更⾼效,推荐以后使⽤emplace系列替代insert和push系列

   
3. 第⼆个程序中我们模拟实现了list的emplace和emplace_back接⼝,这⾥把参数包不段往下传递,最终在结点的构造中直接去匹配容器存储的数据类型T的构造,所以达到了前⾯说的empalce⽀持直接插⼊构造T对象的参数,这样有些场景会更⾼效⼀些,可以直接在容器空间上构造T对象

    
4. 传递参数包过程中,如果是 Args&&... args 的参数包,要⽤完美转发参数包,⽅式如下
std::forward<Args>(args)... ,否则编译时包扩展后右值引⽤变量表达式就变成了左值

push_back和emplace_back的区别:如果只是当前对象的左值和右值的插入,那他们就是一样的

  

如果要将一个临时变量push到容器的末尾:push_back()需要先构造临时对象,再将这个对象拷贝到容器的末尾,emplace_back()则直接在容器的末尾构造对象,这样就省去了拷贝的过程

  

如果构造函数接受一个以上参数,push_back 只接受该类型的唯一对象,而 emplace_back 则接受该类型构造函数的参数

int main()
{list<bit::string> lt;// 传左值,跟push_back⼀样,⾛拷⻉构造bit::string s1("111111111111");lt.emplace_back(s1);cout << "*********************************" << endl;// 右值,跟push_back⼀样,⾛移动构造lt.emplace_back(move(s1));cout << "*********************************" << endl;// 直接把构造string参数包往下传,直接⽤string参数包构造string// 这⾥达到的效果是push_back做不到的lt.emplace_back("111111111111");cout << "*********************************" << endl;list<pair<bit::string, int>> lt1;// 跟push_back⼀样// 构造pair + 拷⻉/移动构造pair到list的节点中data上pair<bit::string, int> kv("苹果", 1);lt1.emplace_back(kv);cout << "*********************************" << endl;// 跟push_back⼀样lt1.emplace_back(move(kv));cout << "*********************************" << endl;// 直接把构造pair参数包往下传,直接⽤pair参数包构造pair// 这⾥达到的效果是push_back做不到的lt1.emplace_back("苹果", 1);cout << "*********************************" << endl;return 0;
}


5. 新的类功能


5.1 默认的移动构造和移动赋值

1. 原来C++类中,有6个默认成员函数:构造函数/析构函数/拷⻉构造函数/拷⻉赋值重载/取地址重载/const 取地址重载,最后重要的是前4个,后两个⽤处不⼤,默认成员函数就是我们不写编译器会⽣成⼀个默认的

   

C++11 新增了两个默认成员函数移动构造函数和移动赋值运算符重载,也就是C++11有8个默认成员函数

    
2. 如果你没有⾃⼰实现移动构造函数,且没有实现析构函数 、拷⻉构造、拷⻉赋值重载中的任意⼀个。那么编译器会⾃动⽣成⼀个默认移动构造

   

默认⽣成的移动构造函数,对于内置类型成员会执⾏逐成员按字节拷⻉,⾃定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调⽤移动构造,没有实现就调⽤拷⻉构造

    
3. 如果你没有⾃⼰实现移动赋值重载函数,且没有实现析构函数 、拷⻉构造、拷⻉赋值重载中的任意⼀个,那么编译器会⾃动⽣成⼀个默认移动赋值。默认⽣成的移动构造函数,对于内置类型成员会执⾏逐成员按字节拷⻉,⾃定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调⽤移动赋值,没有实现就调⽤拷⻉赋值。(默认移动赋值跟上⾯移动构造完全类似)

    
4. 如果你提供了移动构造或者移动赋值,编译器不会⾃动提供拷⻉构造和拷⻉赋值

 

class Person
{
public:Person(const char* name = "", int age = 0):_name(name), _age(age){}/*Person(const Person& p):_name(p._name),_age(p._age){}*//*Person& operator=(const Person& p){if(this != &p){_name = p._name;_age = p._age;
}return *this;
}*//*~Person()
{}*/private:bit::string _name;int _age;
};
int main()
{Person s1;Person s2 = s1;Person s3 = std::move(s1);Person s4;s4 = std::move(s2);return 0;
}


5.2 声明时给缺省值

  


5.3 defult和delete

1. C++11可以让你更好的控制要使⽤的默认函数。假设你要使⽤某个默认的函数,但是因为⼀些原因这个函数没有默认⽣成

    

⽐如:我们提供了拷⻉构造,就不会⽣成移动构造了,那么我们可以使⽤default关键字显⽰指定移动构造⽣成

   
2. 如果能想要限制某些默认函数的⽣成,在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):_name(p._name), _age(p._age){}Person(Person&& p) = default;//Person(const Person& p) = delete;
private:bit::string _name;int _age;
};int main()
{Person s1;Person s2 = s1;Person s3 = std::move(s1);return 0;
}


5.4 final与override 

  这个我在多态的第3.4小结已经有过总结

C++ —— 关于多态-CSDN博客icon-default.png?t=O83Ahttps://blog.csdn.net/hedhjd/article/details/143025744?spm=1001.2014.3001.5501


6. STL中⼀些变化

圈起来的就是新增的一些内容
 

实际最有用的是unordered_map和unordered_set还有右值引用和移动语义相关的push/insert/emplace系列接⼝和移动构造和移动赋值

 


7. lambda


7.1 lambda表达式语法

1. lambda 表达式本质是⼀个匿名函数对象,跟普通函数不同的是他可以定义在函数内部

   
lambda 表达式语法使⽤层⽽⾔没有类型,所以我们⼀般是使用auto或者模板参数定义的对象去接收 lambda 对象

    
2. lambda表达式的格式:

 [capture-list] (parameters)-> return type {function boby }


3. [capture-list] : 捕捉列表,该列表总是出现在 lambda 函数的开始位置,编译器根据[]来
判断接下来的代码是否为 lambda 函数,捕捉列表能够捕捉上下⽂中的变量供 lambda 函数使⽤,捕捉列表可以传值和传引⽤捕捉,捕捉列表为空也不能省略

    
4. (parameters) :参数列表,与普通函数的参数列表功能类似,如果不需要参数传递,则可以连同()⼀起省略

    
5. ->return type :返回值类型,⽤追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可省略。⼀般返回值类型明确情况下,也可省略,由编译器对返回类型进⾏推导

    
6. {function boby} :函数体,函数体内的实现跟普通函数完全类似,在该函数体内,除了可以使⽤其参数外,还可以使⽤所有捕获到的变量,函数体为空也不能省略 
 

// lambda
auto add1 = [rate](int x, int y)->int {return x + y; };
cout << add1(1, 2) << endl;
//add1:lambda对象
//[rate]:lambda的捕捉列表
//(int x, int y):参数列表
//->int:返回类型
//return x + y; :函数体

1、捕捉为空也不能省略               

   

2、参数为空可以省略

    
3、返回值可以省略,可以通过返回对象⾃动推导                

   

4、函数体不能省略 

auto func1 = []{cout << "hello kiana" << endl;return 0;};func1();int a = 0, b = 1;
auto swap1 = [](int& x, int& y){int tmp = x;x = y;y = tmp;};


7.2 捕捉列表

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

   
2. 第⼀种捕捉⽅式是在捕捉列表中显⽰的传值捕捉传引⽤捕捉,捕捉的多个变量用逗号分割

   

[x,y, &z] 表⽰x和y是值捕捉,z是引用捕捉值捕捉的变量不能修改,引⽤捕捉的变量可以修改 ,捕捉列表必须为空,因为全局变量不⽤捕捉就可以⽤,没有可被捕捉的变量

  

int x = 0;
// 捕捉列表必须为空,因为全局变量不⽤捕捉就可以⽤,没有可被捕捉的变量
auto func1 = [](){x++;};int main()
{// 只能⽤当前lambda局部域和捕捉的对象和全局对象int a = 0, b = 1, c = 2, d = 3;auto func1 = [a, &b]{// 值捕捉的变量不能修改,引⽤捕捉的变量可以修改//a++;b++;int ret = a + b;return ret;};cout << func1() << endl;

同一个变量只能捕捉一次

  
3. 第⼆种捕捉⽅式是在捕捉列表中隐式捕捉,我们在捕捉列表写⼀个=表⽰隐式值捕捉,在捕捉列表写⼀个&表⽰隐式引⽤捕捉,这样我们 lambda 表达式中⽤了那些变量,编译器就会⾃动捕捉那些变量

    

// 隐式值捕捉
// ⽤了哪些变量就捕捉哪些变量
auto func2 = [=]{int ret = a + b + c;return ret;};cout << func2() << endl;// 隐式引⽤捕捉
// ⽤了哪些变量就捕捉哪些变量auto func3 = [&]{a++;c++;d++;};
func3();
cout << a << " " << b << " " << c << " " << d << endl;


4. 第三种捕捉⽅式是在捕捉列表中混合使用隐式捕捉和显示捕捉

    

[=, &x]表⽰其他变量隐式值捕捉,x引⽤捕捉;[&, x, y]表⽰其他变量引⽤捕捉,x和y值捕捉

      

使⽤混合捕捉时,第⼀个元素必须是&或=,并且&混合捕捉时,后⾯的捕捉变量必须是值捕捉,同理=混合捕捉时,后⾯的捕捉变量必须是引⽤捕捉 

  

// 混合捕捉1
auto func4 = [&, a, b]{//a++;//b++;c++;d++;return a + b + c + d;};func4();cout << a << " " << b << " " << c << " " << d << endl;// 混合捕捉2
auto func5 = [=, &a, &b]{a++;b++;/*c++;d++;*/return a + b + c + d;};func5();cout << a << " " << b << " " << c << " " << d << endl;

  

5. lambda 表达式如果在函数局部域中,他可以捕捉 lambda 位置之前定义的变量,不能捕捉静态局部变量和全局变量,静态局部变量和全局变量也不需要捕捉, lambda 表达式中可以直接使⽤

   

这也意味着 lambda 表达式如果定义在全局位置捕捉列表必须为空

  

// 局部的静态和全局变量不能捕捉,也不需要捕捉
static int m = 0;
auto func6 = []{int ret = x + m;return ret;};

    
6. 默认情况下, lambda 捕捉列表是被const修饰的,也就是说传值捕捉的过来的对象不能修改,mutable加在参数列表的后⾯可以取消其常量性,也就说使⽤该修饰符后,传值捕捉的对象就可以修改了,但是修改还是形参对象,不会影响实参

    

使⽤该修饰符后,参数列表不可省略(即使参数为空)

  

// 传值捕捉本质是⼀种拷⻉,并且被const修饰了
// mutable相当于去掉const属性,可以修改了
// 但是修改了不会影响外⾯被捕捉的值,因为是⼀种拷⻉
auto func7 = [=]()mutable{a++;b++;c++;d++;return a + b + c + d;};cout << func7() << endl;
cout << a << " " << b << " " << c << " " << d << endl;
return 0;
}


7.3 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 ComparePriceLess
{bool operator()(const Goods& gl, const Goods& gr){return gl._price < gr._price;}
};struct ComparePriceGreater
{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 } };// 类似这样的场景,我们实现仿函数对象或者函数指针⽀持商品中// 不同项的⽐较,相对还是⽐较⿇烦的,那么这⾥lambda就很好⽤了sort(v.begin(), v.end(), ComparePriceLess());sort(v.begin(), v.end(), ComparePriceGreater());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;
}


7.4 lambda的原理

1. lambda 的原理和范围for很像,编译后从汇编指令层的⻆度看,压根就没有 lambda 和范围for这样的东西。范围for底层是迭代器,⽽lambda底层是仿函数对象,也就说我们写了⼀个lambda 以后,编译器会⽣成⼀个对应的仿函数的类

  
2. 仿函数的类名是编译按⼀定规则⽣成的,保证不同的 lambda ⽣成的类名不同,lambda参数/返回类型/函数体就是仿函数operator()的参数/返回类型/函数体, lambda 的捕捉列表本质是⽣成的仿函数类的成员变量,也就是说捕捉列表的变量都是 lambda 类构造函数的实参,当然隐式捕捉,编译器要看使⽤哪些就传那些对象


8. 包装器


8.1 function(包装)

template <class T>
class function;// undefinedtemplate <class Ret, class... Args>
class function<Ret(Args...)>;

std::function 是⼀个类模板,也是⼀个包装器

    

std::function 的实例对象可以包装存储其他的可以调⽤对象,包括函数指针、仿函数、 lambda 、 bind 表达式等,存储的可调⽤对象被称为 std::function 的⽬标

若 std::function 不含⽬标,则称它为空

    

调⽤空std::function 的⽬标导致抛出 std::bad_function_call 异常

 

std::function - cppreference.comicon-default.png?t=O83Ahttps://zh.cppreference.com/w/cpp/utility/functional/functionstd::bad_function_call - cppreference.comicon-default.png?t=O83Ahttps://zh.cppreference.com/w/cpp/utility/functional/bad_function_call

 函数指针、仿函数、 lambda 等可调⽤对象的类型各不相同, std::function 的优势就是统
⼀类型,对他们都可以进⾏包装,这样在很多地⽅就⽅便声明可调⽤对象的类型

 而function有什么用呢?我们可以通过一道题目来了解一下

150. 逆波兰表达式求值 - 力扣(LeetCode)icon-default.png?t=O83Ahttps://leetcode.cn/problems/evaluate-reverse-polish-notation/description/ 传统方式的实现:

// 传统⽅式的实现
class Solution {
public:int evalRPN(vector<string>& tokens) {stack<int> st;for (auto& str : tokens){if (str == "+" || str == "-" || str == "*" || str == "/"){int right = st.top();st.pop();int left = st.top();st.pop();switch (str[0]){case '+':st.push(left + right);break;case '-':st.push(left - right);break;case '*':st.push(left * right);break;case '/':st.push(left / right);break;}}else{st.push(stoi(str));}}return st.top();}
};

使用map映射string和function的方式实现

   
这种方式的最大优势之⼀是方便扩展,假设还有其他运算,我们增加map中的映射即可

class Solution {
public:int evalRPN(vector<string>& tokens) {stack<int> st;// function作为map的映射可调⽤对象的类型map<string, function<int(int, int)>> opFuncMap = {{"+", [](int x, int y) {return x + y; }},{"-", [](int x, int y) {return x - y; }},{"*", [](int x, int y) {return x * y; }},{"/", [](int x, int y) {return x / y; }}};for (auto& str : tokens){if (opFuncMap.count(str)) // 操作符{int right = st.top();st.pop();int left = st.top();st.pop();int ret = opFuncMap[str](left, right);st.push(ret);}else{st.push(stoi(str));}}return st.top();}
};

8.2 bind (绑定)

simple(1)
template <class Fn, class... Args>
/* unspecified */ bind(Fn&& fn, Args&&... args);with return type(2)
template <class Ret, class Fn, class... Args>
/* unspecified */ bind(Fn && fn, Args&&... args);

bind 是⼀个函数模板,它也是⼀个可调⽤对象的包装器,可以把他看做⼀个函数适配器,对接收的fn可调⽤对象进⾏处理后返回⼀个可调⽤对象

   

 bind 可以⽤来调整参数个数和参数顺序

   
bind 也在<functional>这个头⽂件中

调⽤bind的⼀般形式:

   

auto newCallable = bind(callable,arg_list);

其中newCallable本⾝是⼀个可调⽤对象,arg_list是⼀个逗号分隔的参数列表,对应给定的callable的参数

   

当我们调⽤newCallable时,newCallable会调⽤callable,并传给它arg_list中的参数

  

arg_list中的参数可能包含形如_n的名字,其中n是⼀个整数,这些参数是占位符,表⽰
newCallable的参数,它们占据了传递给newCallable的参数的位置

    

数值n表⽰⽣成的可调⽤对象中参数的位置:_1为newCallable的第⼀个参数,_2为第⼆个参数,以此类推:_1/_2/_3....这些占位符放到placeholders的⼀个命名空间中

bind 本质返回的⼀个仿函数对象,调整参数顺序(不常⽤)


1.  _1代表第⼀个实参                 _2代表第⼆个实参  

int Sub(int a, int b)
{return (a - b) * 10;
}int main()
{auto sub1 = bind(Sub, _1, _2);cout << sub1(10, 5) << endl;
}

 2. _2代表第⼀个实参                 _1代表第⼆个实参   

int Sub(int a, int b)
{return (a - b) * 10;
}int main()
{auto sub1 = bind(Sub, _2, _1);cout << sub1(10, 5) << endl;
}

3. 调整参数个数 (常用)


auto sub3 = bind(Sub, 100, _1);
cout << sub3(5) << endl;
auto sub4 = bind(Sub, _1, 100);
cout << sub4(5) << endl;

4. 分别绑死第123个参数

auto sub5 = bind(SubX, 100, _1, _2);
cout << sub5(5, 1) << endl;auto sub6 = bind(SubX, _1, 100, _2);
cout << sub6(5, 1) << endl;auto sub7 = bind(SubX, _1, _2, 100);
cout << sub7(5, 1) << endl;

5. 成员函数对象进⾏绑死,就不需要每次都传递了

function<double(Plus&&, double, double)> f6 = &Plus::plusd;
Plus pd;cout << f6(move(pd), 1.1, 1.1) << endl;cout << f6(Plus(), 1.1, 1.1) << endl;

6.  bind⼀般用于绑死⼀些固定参数

function<double(double, double)> f7 = bind(&Plus::plusd, Plus(), _1, _2);
cout << f7(1.1, 1.1) << endl;

7. 计算复利的lambda

auto func1 = [](double rate, double money, int year)->double {double ret = money;for (int i = 0; i < year; i++){ret += ret * rate;}return ret - money;
};

8. 绑死⼀些参数,实现出⽀持不同年华利率,不同⾦额和不同年份计算出复利的结算利息

function<double(double)> func3_1_5 = bind(func1, 0.015, _1, 3);function<double(double)> func5_1_5 = bind(func1, 0.015, _1, 5);function<double(double)> func10_2_5 = bind(func1, 0.025, _1, 10);function<double(double)> func20_3_5 = bind(func1, 0.035, _1, 30);cout << func3_1_5(1000000) << endl;cout << func5_1_5(1000000) << endl;cout << func10_2_5(1000000) << endl;cout << func20_3_5(1000000) << endl;


完结撒花~

相关文章:

C++ —— 剑斩旧我 破茧成蝶—C++11

江河入海&#xff0c;知识涌动&#xff0c;这是我参与江海计划的第2篇。 目录 1. C11的发展历史 2. 列表初始化 2.1 C98传统的{} 2.2 C11中的{} 2.3 C11中的std::initializer_list 3. 右值引用和移动语义 3.1 左值和右值 3.2 左值引用和右值引用 3.3 引用延长生命周期…...

HTML5好看的音乐播放器多种风格(附源码)

文章目录 1.设计来源1.1 音乐播放器风格1效果1.2 音乐播放器风格2效果1.3 音乐播放器风格3效果1.4 音乐播放器风格4效果1.5 音乐播放器风格5效果 2.效果和源码2.1 动态效果2.2 源代码 源码下载万套模板&#xff0c;程序开发&#xff0c;在线开发&#xff0c;在线沟通 作者&…...

C++设计模式行为模式———迭代器模式中介者模式

文章目录 一、引言二、中介者模式三、总结 一、引言 中介者模式是一种行为设计模式&#xff0c; 能让你减少对象之间混乱无序的依赖关系。 该模式会限制对象之间的直接交互&#xff0c; 迫使它们通过一个中介者对象进行合作。 中介者模式可以减少对象之间混乱无序的依赖关系&…...

FFmpeg 4.3 音视频-多路H265监控录放C++开发十五,解码相关,将h264文件进行帧分隔变成avpacket

前提 前面我们学习了 将YUV数据读取到AVFrame&#xff0c;然后将AVFrame通过 h264编码器变成 AVPacket后&#xff0c;然后将avpacket直接存储到了本地就变成了h264文件。 这一节课&#xff0c;学习解码的一部分。我们需要将 本地存储的h264文件进行帧分隔&#xff0c;也就是变…...

力扣 LeetCode 104. 二叉树的最大深度(Day7:二叉树)

解题思路&#xff1a; 采用后序遍历 首先要区别好什么是高度&#xff0c;什么是深度 最大深度实际上就是根节点的高度 高度的求法是从下往上传&#xff0c;从下往上传实际上就是左右中&#xff08;后序遍历&#xff09; 深度的求法是从上往下去寻找 所以采用从下往上 本…...

如何高效实现汤臣倍健营销云数据集成到SQLServer

新版订单同步-&#xff08;Life-Space&#xff09;江油泰熙&#xff1a;汤臣倍健营销云数据集成到SQL Server 在企业信息化建设中&#xff0c;数据的高效集成和管理是提升业务运营效率的关键。本文将分享一个实际案例——如何通过新版订单同步方案&#xff0c;将汤臣倍健营销云…...

Vue3中使用:deep修改element-plus的样式无效怎么办?

前言&#xff1a;当我们用 vue3 :deep() 处理 elementui 中 el-dialog_body和el-dislog__header 的时候样式一直无法生效&#xff0c;遇到这种情况怎么办&#xff1f; 解决办法&#xff1a; 1.直接在 dialog 上面增加class 我试过&#xff0c;也不起作用&#xff0c;最后用这种…...

具身智能之Isaac Gym使用

0. 简介 Isaac Gym 是由 NVIDIA 提供的一个高性能仿真平台&#xff0c;专门用于大规模的机器人学习和强化学习&#xff08;RL&#xff09;任务。它结合了物理仿真、GPU加速、深度学习框架互操作性等特点&#xff0c;使得研究人员和开发者可以快速进行复杂的机器人仿真和训练。…...

【大数据学习 | Spark】spark-shell开发

spark的代码分为两种 本地代码在driver端直接解析执行没有后续 集群代码&#xff0c;会在driver端进行解析&#xff0c;然后让多个机器进行集群形式的执行计算 spark-shell --master spark://nn1:7077 --executor-cores 2 --executor-memory 2G sc.textFile("/home/ha…...

《Python制作动态爱心粒子特效》

一、实现思路 粒子效果&#xff1a; – 使用Pygame模拟粒子运动&#xff0c;粒子会以爱心的轨迹分布并运动。爱心公式&#xff1a; 爱心的数学公式&#xff1a; x16sin 3 (t),y13cos(t)−5cos(2t)−2cos(3t)−cos(4t) 参数 t t 的范围决定爱心形状。 动态效果&#xff1a; 粒子…...

Jmeter 如何导入证书并调用https请求

Jmeter 如何导入证书并调用https请求 通过SSL管理器添加证书文件 支持添加的文件为.p12&#xff0c;.pfx&#xff0c;.jks 如何将pem文件转换为pfx文件&#xff1f; 在公司内部通常会提供3个pem文件。 ca.pem&#xff1a;可以理解为是根证书&#xff0c;用于验证颁发的证…...

Python程序15个提速优化方法

目录 Python程序15个提速优化方法1. 引言2. 方法一&#xff1a;使用内建函数代码示例&#xff1a;解释&#xff1a; 3. 方法二&#xff1a;避免使用全局变量代码示例&#xff1a;解释&#xff1a; 4. 方法三&#xff1a;使用局部变量代码示例&#xff1a;解释&#xff1a; 5. 方…...

足球虚拟越位线技术FIFA OT(二)

足球虚拟越位线技术FIFA OT&#xff08;二&#xff09; 在FIFA认证测试过程中&#xff0c;留给VAR系统绘制越位线的时间只有90秒&#xff08;在比赛中时间可能更短&#xff09;&#xff0c;那么90秒内要做什么事呢&#xff0c;首先场地上球员做出踢球动作&#xff0c;然后VAR要…...

centos7.9单机版安装K8s

1.安装docker [rootlocalhost ~]# hostnamectl set-hostname master [rootlocalhost ~]# bash [rootmaster ~]# mv /etc/yum.repos.d/* /home [rootmaster ~]# curl -o /etc/yum.repos.d/CentOS-Base.repo http://mirrors.aliyun.com/repo/Centos-7.repo [rootmaster ~]# cu…...

图像编辑一些概念:Image Reconstruction与Image Re-generation

图像编辑本质上是在“图像重建”&#xff08;image reconstruction&#xff09;和“图像再生成”&#xff08;image re-generation&#xff09;之间寻找平衡。 1. Image Reconstruction&#xff08;图像重建&#xff09; 定义&#xff1a;图像重建通常是指从已有的图像中提取信…...

【STM32】在 STM32 USB 设备库添加新的设备类

说实话&#xff0c;我非常想吐槽 STM32 的 USB device library&#xff0c;总感觉很混乱。 USB Device library architecture 根据架构图&#xff1a; Adding a custom class 如果你想添加新的设备类&#xff0c;必须修改的文件有 usbd_desc.cusbd_conf.cusb_device.c 需要…...

【Redis】Redis实现的消息队列

一、用list实现【这是数据类型所以支持持久化】 消息基于redis存储不会因为受jvm内存上限的限制&#xff0c;支持消息的有序性&#xff0c;基于redis的持久化机制&#xff0c;只支持单一消费者订阅&#xff0c;无法避免消息丢失。 二、用PubSub【这不是数据类型&#xff0c;是…...

# Spring事务

Spring事务 什么是spring的事务&#xff1f; 在Spring框架中&#xff0c;事务管理是一种控制数据库操作执行边界的技术&#xff0c;确保一系列操作要么全部成功&#xff0c;要么全部失败&#xff0c;从而维护数据的一致性和完整性。Spring的事务管理主要关注以下几点&#xf…...

Java学习笔记--数组常见算法:数组翻转,冒泡排序,二分查找

一&#xff0c;数组翻转 1.概述:数组对称索引位置上的元素互换&#xff0c;最大值数组序号是数组长度减一 创建跳板temp&#xff0c;进行min和max的互换&#xff0c;然后min自增&#xff0c;max自减&#xff0c;当min>max的时候停止互换&#xff0c;代表到中间值 用代码实…...

ARM 架构(Advanced RISC Machine)精简指令集计算机(Reduced Instruction Set Computer)

文章目录 1、ARM 架构ARM 架构的特点ARM 架构的应用ARM 架构的未来发展 2、RISCRISC 的基本概念RISC 的优势RISC 的应用RISC 与 CISC 的对比总结 1、ARM 架构 ARM 架构是一种低功耗、高性能的处理器架构&#xff0c;广泛应用于移动设备、嵌入式系统以及越来越多的服务器和桌面…...

7.STM32之通信接口《精讲》之USART通信---多字节数据收发(数据包的模式:HEX数据包和文本数据包)

根据上一节的HEX数据包的设计完成&#xff0c;本节将完成文本数据包的编写&#xff0c;&#xff08;HEX数据包其实本质就是原始数据&#xff0c;文本数据包我么要接收到还要对照ASCll进行解析封装&#xff09; 有不懂的可参考上一节的讲解&#xff01;&#xff01;&#xff…...

基于Vue+SpringBoot的求职招聘平台

平台概述 本平台是一个高效、便捷的人才与职位匹配系统&#xff0c;旨在为求职者与招聘者提供一站式服务。平台内设三大核心角色&#xff1a;求职者、招聘者以及超级管理员&#xff0c;每个角色拥有独特的功能模块&#xff0c;确保用户能够轻松完成从信息获取到最终录用的整个…...

WebRTC 和 WebSocket

WebRTC 和 WebSocket 是两种不同的技术&#xff0c;虽然它们都用于在浏览器之间进行通信&#xff0c;但它们的设计目标和使用场景有所不同。以下是它们之间的主要区别&#xff1a; 目的和使用场景 WebRTC: 主要用于实现实时音视频通信。 支持点对点&#xff08;P2P&#xff09…...

小车综合玩法--5.画地为牢

一、实验准备 前面我们利用四路巡线模块巡线&#xff0c;现在我们利用这个特性&#xff0c;用黑线将小车围起来&#xff0c;让小车一直在我们围的圈内运动。 1.小车接线已安装&#xff0c;且安装正确 2.调试四路巡线模块遇黑线时指示灯亮。不是黑线时指示灯灭。 二、实验原理…...

数据库课程设计全流程:方法与实例解析

--- ### 一、数据库课程设计概述 数据库课程设计是学习数据库理论知识的重要实践环节&#xff0c;旨在帮助学生掌握数据库设计和应用系统开发的完整流程&#xff0c;包括需求分析、数据库设计、功能实现以及性能优化。 #### **设计目标** 1. 掌握数据库设计的基本步骤和原则…...

用Ruby编写一个自动化测试脚本,验证网站登录功能的正确性。

测试准备&#xff1a;从江河湖海到代码世界的奇妙之旅 亲爱的朋友们&#xff0c;你们好&#xff01;今天我要带你们进入一个神奇的世界——测试的世界。在这里&#xff0c;我们将会看到各种各样的测试用例&#xff0c;它们就像江河湖海一样&#xff0c;汇聚在一起&#xff0c;…...

跳表 | 基本概念 | 代码实现

文章目录 1.跳表的基本概念2.跳表的结构3.跳表的增删改查4.完整代码 1.跳表的基本概念 跳表的本质是一种查找结构&#xff0c;一般查找问题的解法分为两个大类&#xff1a;一个是基于各种平衡树&#xff0c;一个是基于哈希表&#xff0c;跳表比较的特殊&#xff0c;它独成一派…...

分数加减

#include <stdio.h> #include <stdlib.h>// 求最大公因数 int gcd(int a, int b) {return b 0? a : gcd(b, a % b); }// 化简分数 void simplify(int *num, int *den) {int g gcd(*num, *den);*num / g;*den / g;if (*den < 0) {*num * -1;*den * -1;} }//…...

基于卷积神经网络的皮肤病识别系统(pytorch框架,python源码,GUI界面,前端界面)

更多图像分类、图像识别、目标检测等项目可从主页查看 功能演示&#xff1a; 皮肤病识别系统 vgg16 resnet50 卷积神经网络 GUI界面 前端界面&#xff08;pytorch框架 python源码&#xff09;_哔哩哔哩_bilibili &#xff08;一&#xff09;简介 基于卷积神经网络的皮肤病识…...

QT与嵌入式——获取网络实时时间

目录 1、使用QT通过网络API接口获取网络实时时间 1.1、首先在网上找一个获取实时时间的API接口 1.2、 根据第一步获取的链接来发送请求 1.3、通过connect链接信号与槽 注意的点&#xff1a; 2、为什么需要网络实时时间 3、获取本机的实时时间 4、顺带提一句 1、使用QT通过…...

合肥城市建设网站/百度影响力排名顺序

在C语言中对于float&#xff0c;用4字节存储&#xff0c;比如1.618000 &#xff0c;在内存中为 160 26 207 631.000000&#xff0c; 在内存中为 0 0 128 63那么我有4个字节数据&#xff0c;比如{160 26 207 63}&#xff0c;怎么转成float呢&#xff1f;其…...

没有网站可以做京东联盟吗/如何在百度搜索到自己的网站

第一章 行星地球第一节 宇宙中的地球>>>>一、地球在宇宙中的位置1. 天体是宇宙间物质存在的形式&#xff0c;如恒星、行星、卫星、星云、流星、彗星。2. 天体系统&#xff1a;天体之间相互吸引和相互绕转形成天体系统。3. 天体系统的层次由大到小是&#xff1a;>…...

网站建设服务代理/临沂网站建设方案服务

原标题&#xff1a;一套幼儿园智能化弱电CAD设计图&#xff0c;可以作为投标技术文件模板前言幼儿园中的儿童年龄偏小&#xff0c;家长都迫切希望了解自己家的宝宝在幼儿园的学习和生活情况&#xff0c;因此幼儿园区的智能化弱电系统也成为了幼儿园硬件建设的刚需&#xff0c;今…...

建网站做cpa/搭建一个网站

所用语言与工具代码行工作量&#xff08;人时&#xff09;第一次实验java  Eclipse170  1/2第二次实验java  Eclipse1651/1第三次实验java  Eclipse481/1转载于:https://www.cnblogs.com/mxdmxdmxd78/p/5282232.html...

wordpress 2011/app推广方式有哪些

Install Air Conditioning HDU - 4756 题意是要让n-1间宿舍和发电站相连 也就是连通嘛 最小生成树板子一套 但是还有个限制条件 就是其中有两个宿舍是不能连着的 要求所有情况中最大的那个 这是稠密图 用kruskal的时间会大大增加 所以先跑一遍prim 跑完之后对最小生成树里面的边…...

企业网站建设有名 乐云seo/一站传媒seo优化

今盒子里有n个小球&#xff0c;A、B两人轮流从盒中取球&#xff0c;每个人都可以看到另一个人取了多少个&#xff0c;也可以看到盒中还剩下多少个&#xff0c;并且两人都很聪明&#xff0c;不会做出错误的判断。 我们约定&#xff1a; 每个人从盒子中取出的球的数目必须是&…...