前文中我们讲述了C++11中新增的右值引用,在本文中我们将继续讲解C++11中的新的类功能、可变参数模板、lambda表达式。
新的类功能
默认成员函数
之前我们学习过在C++中有六个默认成员函数构造函数、析构函数、拷贝构造函数、拷贝赋值重载、取地址重载、const 取地址重载。默认成员函数当我们在类中不写的时候编译器就会自动生成。在C++11中新增了两个,移动构造函数和移动赋值运算符重载。
针对移动构造函数和移动赋值运算符重载有一些需要注意的点如下:
- 如果你没有自己实现移动构造函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个。那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造。
- 如果你没有自己实现移动赋值重载函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个,那么编译器会自动生成一个默认移动赋值。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。(默认移动赋值跟上面移动构造完全类似)
- 如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值
类成员变量初始化
C++11允许在类定义时给成员变量初始缺省值,默认生成构造函数会使用这些缺省值初始化。
强制生成默认函数的关键字default
C++11可以让你更好的控制要使用的默认函数。假设你要使用某个默认的函数,但是因为一些原
因这个函数没有默认生成。比如:我们提供了拷贝构造,就不会生成移动构造了,那么我们可以
使用default关键字显示指定移动构造生成。
禁止生成默认函数的关键字delete:
如果能想要限制某些默认函数的生成,在C++98中,是该函数设置成private,并且只声明补丁
已,这样只要其他人想要调用就会报错。在C++11中更简单,只需在该函数声明加上=delete即
可,该语法指示编译器不生成对应函数的默认版本,称=delete修饰的函数为删除函数。
可变参数模板
C++11的新特性可变参数模板能够让您创建可以接受可变参数的函数模板和类模板,相比
C++98,类模版和函数模版中只能含固定数量的模版参数,可变模版参数无疑是一个巨大的改
进。
下面就是一个基本的可变参数的函数模板
// Args是一个模板参数包,args是一个函数形参参数包
// 声明一个参数包Args...args,这个参数包中可以包含0到任意个模板参数。
template <class ...Args>
void ShowList(Args... args)
{}
上面的参数args前面有省略号,所以它就是一个可变模版参数,我们把带省略号的参数称为“参数包”,它里面包含了0到N(N>=0)个模版参数。我们无法直接获取参数包args中的每个参数的,只能通过展开参数包的方式来获取参数包中的每个参数,这是使用可变模版参数的一个主要特点,也是最大的难点,即如何展开可变模版参数。由于语法不支持使用args[i]这样方式获取可变参数,所以我们的用一些奇招来一一获取参数包的值。
递归函数展开方式展开参数包
// 递归终止函数
template <class T>
void ShowList()
{cout << endl;
}
// 展开函数
template <class T, class ...Args>
void ShowList(T value, Args... args)
{cout << value <<" ";ShowList(args...);
}
int main()
{ShowList(1);ShowList(1, 'A');ShowList(1, 'A', std::string("sort"));return 0;
}
逗号表达式展开参数包
这种展开参数包的方式,不需要通过递归终止函数,是直接在expand函数体中展开的, printarg不是一个递归终止函数,只是一个处理参数包中每一个参数的函数。这种就地展开参数包的方式实现的关键是逗号表达式。我们知道逗号表达式会按顺序执行逗号前面的表达式。
expand函数中的逗号表达式:(printarg(args), 0),也是按照这个执行顺序,先执行printarg(args),再得到逗号表达式的结果0。同时还用到了C++11的另外一个特性——初始化列表,通过初始化列表来初始化一个变长数组, {(printarg(args), 0)...}将会展开成((printarg(arg1),0),(printarg(arg2),0), (printarg(arg3),0), etc... ),最终会创建一个元素值都为0的数组int arr[sizeof...(Args)]。由于是逗号表达式,在创建数组的过程中会先执行逗号表达式前面的部分printarg(args)打印出参数,也就是说在构造int数组的过程中就将参数包展开了,这个数组的目的纯粹是为了在数组构造的过程展开参数包。
template <class T>
void PrintArg(T t)
{cout << t << " ";
}
//展开函数
template <class ...Args>
void ShowList(Args... args)
{int arr[] = { (PrintArg(args), 0)... };cout << endl;
}// 编译器编译推演生成的代码
//void ShowList(int a1, char a2, std::string a3)
//{
// int arr[] = { PrintArg(a1), PrintArg(a2), PrintArg(a3) };
// cout << endl;
//}int main()
{ShowList(1);ShowList(1, 'A');ShowList(1, 'A', std::string("sort"));return 0;
}
lambda表达式
在C++98中当我们想要对一个数据集合中的元素进行排序,可以使用std::sort的方法,使用过sort知道该函数默认是按照小于排序的,如果我们需要降序,那么就需要传入一个仿函数来控制比较规则,当我们排序的是自定义类型的元素时也是这样的。下面有这样的一个例子按照正常的方法就是编写不同的仿函数来控制对应的比较规则。
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 } };sort(v.begin(), v.end(), ComparePriceLess());sort(v.begin(), v.end(), ComparePriceGreater());
}
但是这样每次为了实现一个algorithm算法,都要重新去写一个类,如果每次比较的逻辑不一样,还要去实现多个类,特别是相同类的命名,这些都给编程者带来了极大的不便。在C++11中引入了lambda表达式更够简化仿函数的编写。下面我们下来介绍一下lambda的具体写法,然后我们在对上述的例子进行修改。
lambda表达式语法
lambda表达式书写格式:[capture-list] (parameters) mutable -> return-type { statement}
lambda表达式各部分说明
- [capture-list] : 捕捉列表,该列表总是出现在lambda函数的开始位置,编译器根据[]来判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量供lambda函数使用。
- (parameters):参数列表。与普通函数的参数列表一致,如果不需要参数传递,则可以连同()一起省略
- mutable:默认情况下,lambda函数总是一个const函数,mutable可以取消其常量性。使用该修饰符时,参数列表不可省略(即使参数为空)。
- ->returntype:返回值类型。用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推导。
- {statement}:函数体。在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量。
- 注意:
在lambda函数定义中,参数列表和返回值类型都是可选部分,而捕捉列表和函数体可以为
空。因此C++11中最简单的lambda函数为:[]{}; 该lambda函数不能做任何事情。
下面就来展示一下lambda表达式的使用:
int main()
{auto add = [](int x, int y)->int {return x + y; };// 代表lambda对象cout << [](int x, int y)->int {return x + y; }(1, 2) << endl;cout << add(1, 2) << endl;// 省略返回值的类型auto add2 = [](int x, int y){return x + y; };// 最简单的lambda表达式,省略参数、返回值、函数体[] {};return 0;
}
通过上述的例子可以看出,lambda表达式实际上可以理解为无名函数,该函数无法直接调用,如果想要直接调用,可以借助auto将其赋值给一个变量。
捕捉列表说明
捕捉列表描述了上下文中那些数据可以被lambda使用,以及使用的方式传值还是传引用。
- [var]:表示值传递方式捕捉变量var
- [=]:表示值传递方式捕获所有父作用域中的变量(包括this)
- [&var]:表示引用传递捕捉变量var
- [&]:表示引用传递捕捉所有父作用域中的变量(包括this)
- [this]:表示值传递方式捕捉当前的this指针
int main()
{int x = 0; int y = 1;auto func1 = [&x, y]() // 混合捕捉{; };auto func2 = [&]() // 全部引用捕捉{; };auto func3 = [=]() // 全部传值捕捉{; };auto func4 = [&, x]() // 全部传值捕捉, x传值捕捉{; };//auto swap2 = [x, y]() mutable //使传值捕捉可以进行捕获auto swap2 = [&x, &y]() // 捕捉列表可以捕捉外面作用域的变量 // 引用捕捉{int tmp = x;x = y;y = tmp;};swap2();cout << "x:" << x << " y:" << y << endl;
}
lambda函数总是一个const函数,按照上述的例子在捕获之后是不能够进行修改的,但是使用mutable就可以取消其常性,能够进行修改。
注意:
- 父作用域指包含lambda函数的语句块
- 语法上捕捉列表可由多个捕捉项组成,并以逗号分割。比如:
[=, &a, &b]:以引用传递的方式捕捉变量a和b,值传递方式捕捉其他所有变量
[&,a, this]:值传递方式捕捉变量a和this,引用方式捕捉其他变量 - 捕捉列表不允许变量重复传递,否则就会导致编译错误。
比如:[=, a]:=已经以值传递方式捕捉了所有变量,捕捉a重复 - 在块作用域以外的lambda函数捕捉列表必须空。
- 在块作用域中的lambda函数仅能捕捉父作用域中局部变量,捕捉任何非此作用域或者非局部变量都会导致编译报错。
- lambda表达式之间不能相互赋值,即使看起来类型相同
知道了上面的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; });
函数对象与lambda表达式
那么这时可能就会有一个问题,一个lambda表达式究竟有多大,我们可以使用sizeof()函数对其求一下大小,对于这个结果我们可能会以为大小为是四个字节,因为这个lambda的使用与函数指针也是非常相似的。但是实际上它的大小是1字节。这个值就让我们想到了在我们学习类和对象的时候当类的内部没有类型只有函数的时候,它的大小就是1一字节。下面来进行一个实验:有两个相同功能的,一个是类的仿函数,还有一个是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;Rate r1(rate);r1(10000, 2);// lamberauto r2 = [=](double monty, int year)->double {return monty * rate * year;};r2(10000, 2);// lambda+uuid 汇编 每次生成的类的名称都不相同return 0;
}
实际在底层编译器对于lambda表达式的处理方式,完全就是按照函数对象的方式处理的,即:如
果定义了一个lambda表达式,编译器会自动生成一个类,在该类中重载了operator()。这也就是为什么相同功能的lambda表达式不能够互相赋值,因为在实际上这两者是两个不同的类,这个类的类名使用的是lambda+uuid的方式,这样确保每次生成的类的名称都是不相同的。