一. 列表初始化
1.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;
}
1.2 C++11中的{}
- C++11以后想统一初始化的方式,试图实现一切对象皆可用{}初始化,{}初始化也叫列表初始化。
- 内置类型支持,自定义类型也支持,自定义类型的本质是类型转化,先构造一个对象,然后再将构造出来的临时对象拷贝给要初始化的对象,编译器优化后变成了直接构造。

- {}初始化的时候,= 可以省略

- 上面的初始化已经很方便,但是对于对象容器初始化还是不太方便,比如一个vector对象,我们想用N个值去构造初始化,那么需要我们实现多个值的构造函数,C++11库中提出了⼀个std::initializer_list的类, auto il = { 10, 20, 30 }; // the
type of il is an initializer_list ,这个类的本质是底层开一个数组,将数据拷贝过来,std::initializer_list内部有两个指针分别指向数组的开始和结束。这样就可以很方便的初始化一个容器。

二. 右值引用和移动语义
C++98的语法中就有引用的语法,而C++11中新增了右值引用语法特性,C++11之前使用的引用都是左值引用。无论是左值引用还是右值引用,本质上都是给对象取别名。
2.1 左值和右值
- 左值是一个表示数据的表达式(如变量名或解引用的指针),一般是有持久状态,存储在内存中,我们可以获取它的地址,左值可以出现在赋值符号的左边,也可以出现在赋值符号的右边。定义时const修饰符后的左值,不能给它赋值,但是可以取其地址。

- 右值也是一个表示数据的表达式,要么是字面量常量、要么是表达式求值过程中创建的临时对象或者是匿名对象,右值可以出现在赋值符号的右边,但是不能出现在赋值符号的左边,右值不能取地址。

左值和右值最大的区别就是左值可以取地址
2.2 左值引用和右值引用
- Type& r1 = x,是左值引用;Type&& r2 = y,是右值引用。无论是左值引用还是右值引用,本质上都是给对象取别名。
- 左值引用不能直接引用右值,但是const左值引用可以引用右值。
- 右值引用不能直接引用左值,但是右值引用可以引用move(左值)
int main()
{
int* p = new int(0);
int b = 1;
const int c = b;
*p = 10;
string s("111111");
s[0] = 'x';
double x = 1.1, y = 2.2;
// 左值引用给左值取别名
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");
// const左值引用引用右值
const int& rr1 = 10;
const double& rr2 = x + y;
const double& rr3 = fmin(x, y);
const string& rr4 = string("11111");
// 右值引用引用move(左值)
int&& r1 = move(b);
int*&& r2 = move(p);
int&& r3 = move(*p);
string&& r4 = move(s);
char&& r5 = move(s[0]);
return 0;
}

- move是库里面的一个函数模板,本质就是进行强制类型转换,其中也涉及到一些引用折叠的知识,这个后面会细讲。
- 需要注意的是变量表达式都是左值属性,也就意味着一个右值被右值引用绑定后,右值引用变量本身的属性是左值。

rr1的类型是右值引用,但是rr1的属性是左值,因为右值是字面量常量、表达式求值产生的临时对象和匿名对象,所以rr1的属性是左值
- 从语法层面看,左值引用和右值引用都是取别名,不开空间。从汇编层面看,底层都是用指针实现的,没有什么区别。

2.3 引用延长生命周期
右值引用可用于延长临时对象的生命周期,const 的左值引用也能延长临时对象的生命周期,但是这些值都无法修改。
int main()
{
string s1 = "Test";
string& rs1 = s1;
// std::string&& r1 = s1; // 错误:不能绑定到右值
const std::string& r2 = s1 + s1; // OK:const 的左值引用延⻓生存期
// r2 += "Test"; // 错误:不能通过到 const 的引用修改
std::string&& r3 = s1 + s1; // OK:右值引用延⻓生存期
r3 += "Test"; // OK:能通过到非 const 的引用修改,r3的属性是左值,可以修改
std::cout << r3 << '\n';
return 0;
}

2.4 左值和右值的参数匹配
- C++98中,我们实现一个const左值引用作为参数的函数,实参传左值和右值都可以匹配。

- C++11以后,分别重载实现左值引用,const左值引用、右值引用作为形参的函数,函数调用会一一对应。

2.5 右值引用和移动语义的使用场景
2.5.1 左值引用主要的使用场景
左值引用主要使用场景是在函数中左值引用传参和左值引用传返回值时减少拷贝,同时还可以修改实参和修改返回对象的值。左值引用已经解决大多数场景的拷贝效率问题,但是有些场景不能使用传左值引用返回:
class Solution {
public:
// 传值返回需要拷⻉
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());
return str;
}
};
上面这段代码就不能通过传引用返回,因为返回的对象是在本函数栈帧创建的,出了函数作用域就会被销毁,在调用函数访问该函数的返回值就和访问野指针一样了,右值引用虽然可以延长生命周期,但是出了作用域被销毁后,生命周期也就结束了。

2.5.2 移动语义和移动构造
- 移动构造函数是一种构造函数,类似于拷贝构造函数,移动构造函数要求第一个参数是该类类型的右值引用,如果还有其它参数,额外的参数必须有缺省值。

有了移动构造之后,上述的addStrings函数就可以通过移动构造返回结果,不需要再去进行拷贝构造了,提高了效率。 - 移动赋值是一个赋值运算符的重载,他跟拷贝赋值构成函数重载,类似于拷贝赋值函数,移动赋值函数要求第一个参数是该类类型的右值引用。

移动赋值的本质也是进行交换,不再去开空间挨个赋值,提高了效率。
- 右值对象构造,只有拷贝构造,没有移动构造的场景
- 下图展示了vs2019 debug环境下编译器对拷贝的优化,左边为不优化的情况下,两次拷贝构造,右边为编译器优化的场景下连续步骤中的拷贝合二为一变为一次拷贝构造。
- 需要注意的是在vs2019的release和vs2022的debug和release,下面代码优化为非常恐怖,会直接将str对象的构造,str拷贝构造临时对象,临时对象拷贝构造ret对象,合三为一,变为直接构造。要理解这个优化要结合局部对象生命周期和栈帧的角度理解,如图3所示。
- linux下可以将下面代码拷贝到test.cpp文件,编译时用g++ test.cpp -fno-elideconstructors的方式关闭构造优化,运行结果可以看到图1左边没有优化的两次拷贝。

- 右值对象构造,有拷贝构造,也有移动构造的场景
- 下图展示了vs2019 debug环境下编译器对拷贝的优化,左边为不优化的情况下,两次移动构造,右边为编译器优化的场景下连续步骤中的拷贝合二为一变为一次拷贝构造。
- 需要注意的是在vs2019的release和vs2022的debug和release,下面代码优化为非常恐怖,会直接将str对象的构造,str拷贝构造临时对象,临时对象拷贝构造ret对象,合三为一,变为直接构造。要理解这个优化要结合局部对象生命周期和栈帧的角度理解,如图3所示。
- linux下可以将下面代码拷贝到test.cpp文件,编译时用g++ test.cpp -fno-elideconstructors的方式关闭构造优化,运行结果可以看到图1左边没有优化的两次拷贝。

图3

2.6 类型分类
- C++11以后,进一步对类型进行了划分,右值被划分为纯右值(pure value,简称prvalue)和将亡值(expiring value,简称xvalue)。
- 纯右值是指那些字面值常量或求值结果相当于字面值或者一个不具名的临时对象。如100、true、nullptr或者str.substr(1,2)、str1 + str2 传值返回函数调用,或者整型a+b,a++等。纯右值和将亡值是C++11中提出的,C++11中的纯右值概念划分等价于C++98的右值。
- 将亡值是指返回右值引用的函数调用表达式和转换为右值引用的转换函数的调用表达,如move(x)
- 泛左值(generalized value,简称gvalue),泛左值包含将亡值和左值。
- 有名字,就是gvalue;有名字,且不能被move,就是lvalue;有名字,且可以被move,就是xvalue;没有名字,且可以被移动就是prvalue。

2.7 引用折叠
- C++中不能直接定义引用的引用如: int& && r = i;这样写会直接报错,通过模板或typedef中的类型操作可以构成引用的引用,
- 通过模板或typedef中的类型操作可以构成引用的引用时,这时C++11给出了一个引用折叠的规则:右值引用的右值引用折叠成右值引用,其他所有组合均折叠为左值引用

通过示例,我们来理解一下引用折叠
template<class T>
void f1(T& x)
{}
int main()
{
int n = 0;
// 没有折叠->实例化为void f1(int& x)
f1<int>(n);// T推导为int
f1<int>(0); // 报错,将右值传给左值
// 折叠->实例化为void f1(int& x)
f1<int&>(n); // T推导为int&,左值和左值折叠还是左值
f1<int&>(0); // 报错,将右值赋给左值
// 折叠->实例化为void f1(int& x)
f1<int&&>(n); // T推导为int
f1<int&&>(0); // 报错,右值引用和左值引用折叠后时左值引用,将右值传给左值
// 折叠->实例化为void f1(const int& x)
f1<const int&>(n);
f1<const int&>(0);// 有了const
// 折叠->实例化为void f1(const int& x)
f1<const int&&>(n);
f1<const int&&>(0);// 有了const
return 0;
}

由于引用折叠限定,f1实例化以后总是⼀个左值引用

由于引用折叠限定,f2实例化后可以是左值引用,也可以是右值引用,像f2这样的函数模板中,T&& x参数看起来是右值引用参数,但是由于引用折叠的规则,他传递左
值时就是左值引用,传递右值时就是右值引用,有些地方也把这种函数模板的参数叫做万能引用。
在发生引用折叠的过程中,传左值模板参数T会被推导为T&,而传右值模板参数T会被推导为T


Function(T&& t)函数模板程序中,假设实参是int右值,模板参数T的推导int,实参是int左值,模板参数T的推导int&,再结合引用折叠规则,就实现了实参是左值,实例化出左值引用版本形参的Function,实参是右值,实例化出右值引用版本形参的Function。
2.8 完美转发
-
Function(T&& t)函数模板中,传左值实例化以后就是左值引用的Function函数,传右值实例化以后就是右值引用的Function函数。
-
右值引用的属性是左值,也就意味着一个右值被右值引用绑定后,t的属性就是左值

像上面这样,我们传一个右值给Function,然后继续传给Fun函数,右值应该调用右值版本的函数,但是调用了左值引用的函数,这是因为右值引用的属性是左值,也就是说t的属性是左值,向下传递就会调用左值版本的函数。 -
为了保持对象原有的属性,我们需要完美转发实现。
template <class T> T&& forward (typename remove_reference<T>::type& arg); -
完美转发forward本质是⼀个函数模板,他主要还是通过引用折叠的方式实现,下面示例中传递给Function的实参是右值,T被推导为int,没有折叠,forward内部t被强转为右值引用返回;传递给Function的实参是左值,T被推导为int&,引用折叠为左值引用,forward内部t被强转为左值引用返回
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>
void Function(T && t)
{
Fun(forward<T>(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& t)
Function(b); // const 左值,完美转发强转为const左值返回
// std::move(b)右值,推导出T为const int,模板实例化为void Function(const int&& t)
Function(std::move(b)); // const 右值,完美转发强转为const右值返回
return 0;
}

三. 可变参数模板
3.1 基本语法及原理
- 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…指出接下来的参数表示零个或多个类型列表;在函数参数列表中,类型名后面跟…指出接下来表示零个或多个形参对象列表;函数参数包可以用左值引用或右值引用表示,跟前面普通模板一样,每个参数实例化时遵循引用折叠原则。
- 可变模板参数的原理跟模板类似,本质还是去实例化对应类型和个数的多个函数。
- 我们可以使用sizeof…运算符来计算参数包中参数的个数。


3.2 包扩展
- 对于一个参数包,我们除了能计算它的参数个数,我们唯一能做的就是扩展它,当扩展一个包时,我们还要提供用于每个扩展元素的模式,扩展一个包就是将它分解为构成的元素,对每个元素应用模式,获得扩展后的列表。我们通过在模式的右边放一个省略号(…)来触发扩展操作。
void ShowList()
{
// 编译器时递归的终止条件,参数包是0个时,直接匹配这个函数
cout << endl;
}
template <class T, class ...Args>
void ShowList(T x, Args... args)
{
/*if (sizeof...(args) == 0)
return;*/
cout << x << " ";
// args是N个参数的参数包
// 调用ShowList,参数包的第一个传给x,剩下N-1传给第二个参数包
ShowList(args...);
}
// 编译时递归推导解析参数
template <class ...Args>
void Print(Args... args)
{
ShowList(args...);
}
int main()
{
Print(1, string("xxxxx"), 2.2);
return 0;
}

这里传空包的时候不能使用if(sizeof...(args) == 0) return;的原因是因为这是运行时递归的逻辑,但是包扩展递归是编译时递归。
对于参数包,我们不能按照数组的方式取到,有一个原因是C++11不允许容器村不同类型的数据
- C++还支持更复杂的包扩展,直接将参数包依次展开依次作为实参给一个函数去处理。
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)...);
}
// 本质可以理解为编译器编译时,包的扩展模式
// 将上面的函数模板扩展实例化为下面的函数
//void Print(int x, string y, double z)
//{
// Arguments(GetArg(x), GetArg(y), GetArg(z));
//}
int main()
{
Print(1, string("xxxxx"), 2.2);
return 0;
}
参数包里有几个参数就调用几次GetArg函数
3.3 emplace接口
template <class... Args> void emplace_back (Args&&... args);template <class... Args> void emplace_back (Args&&... args);- C++11以后STL容器新增了emplace系列的接口,emplace系列的接口均为可变模板参数,功能上兼容push系列和insert系列,但是emplace系列还支持新玩法,假设容器为container,emplace还支持直接插入构造T对象的参数,这样有些场景会更高效一些,可以直接在容器空间上构造T对象。

总结:
emplace系列兼容push系列和insert系列的功能,部分场景下emplace系列可以直接构造,push系列和insert是构造+移动构造或者构造+拷贝构造,所以emplace综合而言效率较另外两者效率会更高,更推荐使用emplace系列来代替push和insert系列。
emplace系列的模拟实现
template<class T>
struct ListNode
{
template<class...Args>
ListNode(Args&& ...args)
:_next(nullptr)
,_prev(nullptr)
,_data(std::forward<Args>(args)...)
{}
};
template<class T>
class list{
template<class...Args>
iterator insert(iterator pos, Args...args)
{
Node* cur = pos._node;
Node* newnode = new Node(std::forward<Args>(args)...);
// prev newnode cur
Node* prev = cur->_prev;
newnode->_prev = prev;
prev->_next = newnode;
newnode->_next = cur;
cur->_prev = newnode;
return newnode;
}
template<class...Args>
void emplace_back(Args...args)
{
insert(end(), std::forward<Args>(args)...);
}
};

四. 新的类功能
4.1 默认的移动构造和移动赋值
- 原来C++类中,有6个默认成员函数:构造函数/析构函数/拷贝构造函数/拷贝赋值重载/取地址重载/const取地址重载,最重要的是前4个,后两个用处不大,默认成员函数就是我们不写编译器会生成一个默认的。C++11新增了两个默认成员函数,移动构造函数和移动赋值运算符重载。
- 如果没有自己实现移动构造函数,且没有实现析构函数、拷贝构造、拷贝赋值重载,那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造。
- 如果没有自己实现移动赋值重载函数,且没有实现析构函数、拷贝构造、拷贝赋值重载,那么编译器会自动生成一个默认移动赋值。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。(默认移动赋值跟上面移动构造完全类似)

只有在自己没有实现移动赋值重载函数,且没有实现析构函数、拷贝构造、拷贝赋值重载的时候,编译器才会自动生成一个默认移动赋值。
4.2 声明时给缺省值
这个缺省值主要是给没有显示在初始化列表初始化的成员使用的,编译器自己生成的默认构造函数也会使用这个缺省值。

总结:
-
成员变量显式在初始化列表初始化(传参就用参数,没传参就用列表的缺省值):
- 构造函数参数列表有缺省值且对象初始化时传参,成员变量声明时有缺省值,使用传递的参数初始化
- 构造函数参数列表有缺省值但对象初始化时没传参,成员变量声明时有缺省值,使用构造函数列表的缺省值初始化
- 构造函数参数列表有缺省值但对象初始化时传参,成员变量声明时没有缺省值,使用传递的参数初始化
- 构造函数参数列表有缺省值但对象初始化时不传参,成员变量声明时没有缺省值,使用构造函数列表的缺省值初始化
- 构造函数参数列表没有缺省值,成员变量声明时有缺省值,对象初始化时传参,使用传递的参数初始化
- 构造函数参数列表没有缺省值,成员变量声明时有缺省值,对象初始化时不传参,报错
- 构造函数参数列表没有缺省值,成员变量声明时也没有缺省值,对象初始化时传参,使用传递的参数初始化
- 构造函数参数列表没有缺省值,成员变量声明时也没有缺省值,对象初始化时不传参,报错

-
成员变量没有显式在初始化列表初始化:
- 成员声明时有缺省值,就用声明时的缺省值初始化
- 成员声明时没有缺省值,初始化随机值
- 如果既没传参,且构造函数的参数列表没有缺省值,成员变量有没有缺省值都会报错
4.3 default和delete
-
C++11可以让我们更好的控制要使用的默认函数。假如要使用某个默认的函数,但是因为一些原因这个函数没有默认生成。比如:我们写了拷贝构造,就不会生成移动构造了,那么我们就可以使用default关键字显式指定移动构造生成。

-
如果想要限制某些默认函数的生成,在C++98中,是将该函数设置成private,并且只声明不实现,这样别人想要调用就会报错。在C++11中,更简单,只需在该函数声明上加上=delete即可,该语法指示编译器不生成对应函数的默认版本,称=delete修饰的函数为删除函数。


4.4 final和override
五. STL中的一些变化
- C++11中,STL新增了下面圈划出的容器,但是最有用的是unordered_map和unordered_set。

- STL中容器的新增接口也不少,最重要的就是右值引用和移动语义相关的push/insert/emplace系列接口和移动构造和移动赋值,还有initializer_list版本的构造等。
- 容器的范围for便利。
六. lambda
6.1 lambda表达式语法
lambda表达式本质是一个匿名函数对象,跟普通函数不同的是他可以定义在函数内部。lambda表达式语法使用层而言没有类型(它的类型是编译器使用uuid生成的字符串),所以我们一般使用auto或者模板参数定义的对象去接受lambda对象。lambda表达式的格式:[capture-list] (parameters)-> return type { function boby }[capture-list]:捕捉列表,该列表总是出现在lambda函数的开始位置,编译器根据[]来判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量供lambda函数使用,捕捉列表可以传值和传引用捕捉,具体使用我们后面讲。捕捉列表为空也不能省略。(parameters):参数列表,与普通函数的参数列表功能类似,如果不需要参数传递,则可以和()一起省略。->return type:返回值类型,用追踪返回类型形式声明函数的返回值类型,没有返回值时,此部分可以省略。一般返回值类型明确的情况下,也可省略,由编译器对返回类型进行推导。{function body}:函数体,函数体内的实现跟普通函数完全类似,在该函数体内,除了可以使用其参数外,还可以使用所有捕捉到的变量,函数体为空也不能省略。
一个简单的lambda:
int main()
{
auto add = [](int a, int b)->int {return a + b; };
cout << add(1, 1) << endl;
return 0;
}

6.2 捕捉列表
-
lambda表达式中默认只能用lambda函数体和参数中的变量

如果想用外层作用域中的变量就需要进行捕捉 -
第一种方式是在捕捉列表中显示的传值捕捉和传引用捕捉,捕捉的多个变量用逗号分隔。[x,y,&z]表示x和y值捕捉,z引用捕捉。
int main()
{
int a = 0, b = 1, c = 2, d = 3;
auto func1 = [a, &b]
{
a++;// a是值捕捉
b++;// b是引用捕捉
int ret = a + b;
return ret;
};
return 0;
}


一个变量也不能捕捉多次
- 第二种方式是在捕捉列表中隐式捕捉,我们在捕捉列表写一个=表示隐式值捕捉,在捕捉列表中写一个&表示隐式引用捕捉,这样我们
lambda表达式中用了哪些变量,编译器就会捕捉哪些变量。
int main()
{
int a = 0, b = 1, c = 2, d = 3;
// 隐式值捕捉
// 用了哪些变量就捕捉哪些变量
auto func1 = [=]
{
int ret = a + b + c;
return ret;
};
// 隐式引用捕捉
auto func2 = [&]
{
int ret = a + b + c;
return ret;
};
return 0;
}

这里对所有的变量隐式引用捕捉,实际上只捕捉了用到的变量。
- 第三种方式就是在捕捉列表中混合使用隐式捕捉和显示捕捉。[=,&x]表示其他变量隐式值捕捉,x引用捕捉;[&,x,y]表示其他变量引用捕捉,x和y值捕捉。当使用混合捕捉时,第一个元素必须是&或=,并且&混合捕捉时,后面的捕捉变量必须是值捕捉,同理=混合捕捉时,后面的捕捉变量必须是引用捕捉。

lambda表达式如果在函数局部域中,它可以捕捉lambda位置之前定义的变量,不能捕捉静态局部变量和全局变量,静态局部变量和全局变量也不需要捕捉,lambda表达式中可以直接使用。也就意味着lambda表达式如果定义在全局位置,捕捉列表必须为空(因为定义在全局的 lambda 表达式无法捕获任何局部变量,但可以直接访问全局变量)。

- 默认情况下。
lambda捕捉列表是被const修饰的,也就是说传值捕捉过来的对象不能修改,mutable加在参数列表的后面可以取消其常量性,也就是说使用该修饰符后,传值捕捉的对象就可以修改了,但是修改的还是形参对象,不会影响实参,想要改变实参还是需要引用捕捉。使用该修饰符后,参数列表任何情况下都不能省略。

6.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)
{}
};
对于上面Goods类的对象,我们如果想对价格、评价排序的话,需要我们写一个仿函数然后传仿函数对象给sort函数,有了lambda表达式之后,我们可以写一个lambda传给sort进行比较。
int main()
{
vector<Goods> v = { { "苹果", 2.1, 5 }, { "香蕉", 3, 4 },
{ "橙子", 2.2, 3}, { "菠萝", 1.5, 4 } };
// 价格降序
sort(v.begin(), v.end(), [](Goods& a, Goods& b) {return a._price > b._price;});
// 价格升序
sort(v.begin(), v.end(), [](Goods& a, Goods& b) {return a._price < b._price; });
// 评价降序
sort(v.begin(), v.end(), [](Goods& a, Goods& b) {return a._evaluate > b._evaluate; });
// 评价升序
sort(v.begin(), v.end(), [](Goods& a, Goods& b) {return a._evaluate < b._evaluate; });
return 0;
}
lambda在很多其他地方起来也很好用。比如线程中定义线程的执行函数逻辑,智能指针中定制删除器等,lambda的应用还是很广泛的,以后我们会不断接触到。
6.4 lambda的原理
lambda的原理和范围for很像,编译后从汇编指令层的角度看,压根就没有lambda和范围for。范围for的底层是迭代器,而lambda底层是仿函数对象,也就是说我们写了一个lambda后,编译器会在编译时生成一个仿函数的类。- 仿函数的类名是编译器按一定规则生成的,保证不同的
lambda生成的类名不同,lambda参数/返回值类型/函数体就是仿函数operator()的参数/返回类型/函数体,lambda的捕捉列表的本质是生成仿函数类的成员变量,也就是说捕捉列表的变量都是lambda类构造函数的实参,当然隐式捕捉,编译器就要看使用哪些对象就传哪些对象。
class Rate
{
public:
Rate(double rate)
: _rate(rate)
{}
double operator()(double money, int year)
{
return money * _rate * year;
}
private:
double _rate;
};
int main()
{
double rate = 0.49;
// lambda
auto r2 = [rate](double money, int year) {
return money * rate * year;
};
// 函数对象
Rate r1(rate);
r1(10000, 2);
r2(10000, 2);
return 0;
}

七. 包装器
7.1 function
std::function是一个类模板,也是一个包装器。std::function的实例对象可以包装存储其他的可调用对象,包括函数指针、仿函数、lambda、bind表达式等,存储的可调用对象被称为std::function的目标。若std::function不含目标,则称它为空。调用空std::function的目标导致抛出std::bad_function_call异常。- 以下是
std::function的原型,他被定义在<functional>头文件中template <class T> class function; // undefined template <class Ret, class... Args> class function<Ret(Args...)>;
- 函数指针、仿函数、
lambda等可调用对象的类型各不相同,std::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;// 包装全局函数
function<int(int, int)> f2 = Functor();// 包装仿函数
function<int(int, int)> f3 = [](int a, int b) {return a + b; };// 包装lambda
cout << f1(1, 1) << endl;
cout << f2(1, 1) << endl;
cout << f3(1, 1) << endl;
return 0;
}
怎么使用function包装类的成员函数呢?
包装静态成员函数和普通的函数一样,但是包装普通的成员函数需要传对象或者对象的指针,因为一般的成员函数有this指针,所以为了参数匹配,要传对象或对象的指针,然后对象再去调用成员函数。
#include<functional>
int f(int a, int b)
{
return a + b;
}
struct Functor
{
public:
int operator() (int a, int b)
{
return a + b;
}
};
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()
{
// 包装静态成员函数
// 静态成员函数要指定类域并且前面加&才能获取地址
function<int(int, int)> f4 = &Plus::plusi;
cout << f4(1, 1) << endl;
// 包装普通成员函数
// 普通成员函数还有一个隐含的this指针参数,所以绑定时传对象或者对象的指针过去都可以
function<double(Plus*, double, double)> f5 = &Plus::plusd;
Plus pd;
cout << f5(&pd, 1.1, 1.1) << endl;
function<double(Plus, double, double)> f6 = &Plus::plusd;
cout << f6(pd, 1.1, 1.1) << endl;
cout << f6(pd, 1.1, 1.1) << endl;
function<double(Plus&&, double, double)> f7 = &Plus::plusd;// 实践中喜欢这样使用,可以传匿名对象
cout << f7(move(pd), 1.1, 1.1) << endl;
cout << f7(Plus(), 1.1, 1.1) << endl;
return 0;
}
有了包装器function之后,对于下面这道题就很容易解决了
逆波兰表达式求值

class Solution {
public:
int evalRPN(vector<string>& tokens) {
int ret = 0;
stack<int> st;
map<string,function<int(int,int)>>mp=
{
{"+",[](int a,int b){return a + b;}},
{"-",[](int a,int b){return a - b;}},
{"*",[](int a,int b){return a * b;}},
{"/",[](int a,int b){return a / b;}},
};
for(auto& s : tokens)
{
if(mp.count(s))
{
int right = st.top();
st.pop();
int left = st.top();
st.pop();
ret = mp[s](left,right);
st.push(ret);
}
else
{
st.push(stoi(s));
}
}
return st.top();
}
};
7.2 bind
-
bind是一个函数模板,它也是一个可调用对象的包装器,可以把它看成一个函数适配器,对接收的fn可调用对象进行处理后返回一个可调用对象。bind可以用来调整参数个数和参数顺序,它也定义在functional头文件中。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的一般形式:
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的使用 -
调整参数顺序

-
调整参数个数
#include<functional>
using placeholders::_1;
using placeholders::_2;
using placeholders::_3;
int Sub(int a, int b)
{
return (a - b) * 10;
}
int SubX(int a, int b, int c)
{
return (a - b - c) * 10;
}
class Plus
{
public:
static int plusi(int a, int b)
{
return a + b;
}
double plusd(double a, double b)
{
return a + b;
}
};
int main()
{
auto sub3 = bind(Sub, 100, _1);// 将参数绑死a始终为100
cout << sub3(5) << endl;// 100 - 5
auto sub4 = bind(Sub, _1, 100);// 将参数绑死b始终为100
cout << sub4(5) << endl;// 5 - 100
// 分别绑死第123个参数
auto sub5 = bind(SubX, 100, _1, _2);
cout << sub5(5, 1) << endl;// (100 - 5 - 1) * 10
auto sub6 = bind(SubX, _1, 100, _2);
cout << sub6(5, 1) << endl;// (5 - 100 - 1) * 10
auto sub7 = bind(SubX, _1, _2, 100);
cout << sub7(5, 1) << endl;// (5 - 1 - 100) * 10
return 0;
}
对于function包装的成员函数对象,我们可以使用bind绑死,这样就不用每次传参的时候传递了
int main()
{
function<double(Plus&&, double, double)> f1 = &Plus::plusd;
cout << f1(Plus(), 2.2, 3.3) << endl;
// bind
function<double(double, double)> f2 = bind(&Plus::plusd,Plus(), _1, _2);
cout << f2(2.2, 3.3) << endl;
return 0;
}

- 绑定lambda对象
int main()
{
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;
};
// 绑死一些参数,实现出支持不同年华利率,不同金额和不同年份计算出复利的结算利息
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);
// 这样function包装的bind绑定的对象时,因为bind绑死了三个参数,
// 只需再传一个参数给money,所以function只需再传一个参数即可
cout << func3_1_5(1000000) << endl;
cout << func5_1_5(1000000) << endl;
cout << func10_2_5(1000000) << endl;
cout << func20_3_5(1000000) << endl;
return 0;
}
结语
以上我们就讲完了C++11常用的内容。感谢大家的阅读,欢迎大家批评指正!
C++11新特性全面解析

被折叠的 条评论
为什么被折叠?



