自学内容网 自学内容网

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 rvalue
return 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必须返回或者到的对象,这样才能组成参数包给Arguments
Arguments(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;// undefined

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


完结撒花~


原文地址:https://blog.csdn.net/hedhjd/article/details/143805396

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!