lambda表达式
C 98中的一个例子
在C 98中,如果想要对一个数据集合中的元素进行排序,可以使用std::sort方法。
代码语言:javascript复制#include <algorithm>
#include <functional>
int main()
{
int array[] = {4,1,8,5,3,7,0,9,2,6};
// 默认按照小于比较,排出来结果是升序
std::sort(array, array sizeof(array)/sizeof(array[0]));
// 如果需要降序,需要改变元素的比较规则
std::sort(array, array sizeof(array) / sizeof(array[0]), greater<int>());
return 0;
}
如果待排序元素为自定义类型,需要用户定义排序时的比较规则:
代码语言:javascript复制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());
}
随着C 语法的发展,人们开始觉得上面的写法太复杂了,每次为了实现一个algorithm
算法,都要重新去写一个类,如果每次比较的逻辑不一样,还要去实现多个类,特别是相同类的命名,这些都给编程者带来了极大的不便。因此,在C 11语法中出现了Lambda
表达式。
Lambda表达式基础语法
lambda
表达式书写格式:[capture-list] (parameters) mutable -> return-type { statement}
lambda表达式各部分说明
[capture-list]
: 捕捉列表,该列表总是出现在lambda函数的开始位置,编译器根据[]来判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量供lambda函数使用。
(parameters)
:参数列表。与普通函数的参数列表一致,如果不需要参数传递,则可以连同()一起省略
mutable
:默认情况下,lambda函数总是一个const函数,mutable可以取消其常量性。使用该修饰符时,参数列表不可省略(即使参数为空)。
->returntype
:返回值类型。用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推导。
{statement}
:函数体。在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量。
int main()
{
auto add1 = [](int a, int b)->int {return a b; };
auto add2 = [](int a, int b){return a b; }; //返回值省略
auto func1 = [] {cout << "hello world" << endl; }; // 没有参数,参数列表可以省略
cout << add1(1, 2) << endl;
cout << add2(2, 3) << endl;
func1();
return 0;
}
通过上述例子可以看出,lambda表达式实际上可以理解为无名函数,该函数无法直接调用,如果想要直接调用,可借助auto将其赋值给一个变量。
将上述排序修改成lambda表达式:
代码语言:javascript复制int main()
{
vector<Goods> v = { { "苹果", 2.1, 5 }, { "香蕉", 3, 4 }, { "橙子", 2.2,
3 }, { "菠萝", 1.5, 4 } };
auto priceLess = [](const Goods& g1, const Goods& g2) {return g1._price < g2._price; };
sort(v.begin(), v.end(), priceLess);
}
捕获列表说明
捕捉列表描述了上下文中那些数据可以被lambda使用,以及使用的方式传值还是传引用。
-
[var]
:表示值传递方式捕捉变量var
-
[=]
:表示值传递方式捕获所有父作用域中的变量(包括this
)
int main()
{
int a = 1, b = 2, c = 3, d = 4, e = 5;
auto func1 = [=]()
{
return a b c d e;
};
cout << func1();
return 0;
}
[&var]
:表示引用传递捕捉变量var
int main()
{
int a = 1, b = 2;
//引用方式捕捉
auto swap3 = [&a, &b]()
{
int tmp = a;
a = b;
b = tmp;
};
swap3();
return 0;
}
引用捕捉,swap3 = [&a, &b]()
这里的a
和b
就是外部a
和b
的别名,里面发生改变必定导致外面的a
和b
发生变化。
[&]
:表示引用传递捕捉所有父作用域中的变量(包括this
)
int main()
{
int a = 1, b = 2, c = 3, d = 4, e = 5;
auto func2 = [&]()
{
a ;
b ;
c ;
d ;
e ;
};
func2();
cout << a << b << c << d << e << endl;
return 0;
}
[this]
:表示值传递方式捕捉当前的this
指针
注意:
- 父作用域指包含lambda函数的语句块
- 语法上捕捉列表可由多个捕捉项组成,并以逗号分割。 比如:[=, &a, &b]:以引用传递的方式捕捉变量a和b,值传递方式捕捉其他所有变量[&,a, this]:值传递方式捕捉变量a和this,引用方式捕捉其他变量
- 捕捉列表不允许变量重复传递,否则就会导致编译错误。比如:[=, a]:=已经以值传递方式捕捉了所有变量,捕捉a重复。
- 在块作用域以外的lambda函数捕捉列表必须为空。
- 在块作用域中的lambda函数仅能捕捉父作用域中局部变量,捕捉任何非此作用域或者非局部变量都会导致编译报错。
- lambda表达式之间不能相互赋值,即使看起来类型相同
新的类功能
默认成员函数
原来C 类中,有6个默认成员函数:
- 构造函数
- 析构函数
- 拷贝构造函数(深拷贝)
- 拷贝赋值重载(深拷贝)
- 取地址重载
- const 取地址重载
最后重要的是前4个,后两个用处不大。默认成员函数就是我们不写编译器会生成一个默认的。C 11 新增了两个:移动构造函数(移动拷贝)和移动赋值运算符重载(移动拷贝)。这个针对的是深拷贝的自定义类型对象,如:string
、vector
、list
等。
针对移动构造函数和移动赋值运算符重载有一些需要注意的点如下:
- 如果你没有自己实现移动构造函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个。那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数,(1)对于内置类型成员会执行逐成员按字节拷贝,(2)自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造。
- 如果你没有自己实现移动赋值重载函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个,那么编译器会自动生成一个默认移动赋值。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。(默认移动赋值跟上面移动构造完全类似)
- 如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值
强制生成默认函数的关键字default
C 11可以让你更好的控制要使用的默认函数。假设你要使用某个默认的函数,但是因为一些原因这个函数没有默认生成。比如:我们提供了拷贝构造,就不会生成移动构造了,那么我们可以使用default关键字显示指定移动构造生成.
代码语言:javascript复制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;
private:
gwj::string _name;
int _age;
};
int main()
{
Person s1;
Person s2 = s1;
Person s3 = std::move(s1);
return 0;
}
禁止生成默认函数的关键字delete
如果能想要限制某些默认函数的生成,在C 98中,是该函数设置成private,并且只声明补丁已,这样只要其他人想要调用就会报错。在C 11中更简单,只需在该函数声明加上=delete即可,该语法指示编译器不生成对应函数的默认版本,称=delete修饰的函数为删除函数。
实现一个类只能在堆上创建对象:
代码语言:javascript复制class HeapOnly
{
public:
// 静态方法,用于在堆上创建 HeapOnly 对象
static HeapOnly* CreateObj()
{
return new HeapOnly;
}
private:
// 私有构造函数,禁止在类外部直接创建对象
HeapOnly()
{}
int _a = 1;
};
int main()
{
// HeapOnly ho1; // 错误:构造函数是私有的,无法在类外部直接创建对象
// HeapOnly* p1 = new HeapOnly; // 错误:构造函数是私有的,无法直接调用 new 操作符
HeapOnly* p2 = HeapOnly::CreateObj(); // 正确:通过静态方法创建对象
return 0;
}
这个方法仍然有bug,不能确保创建的类是在堆上:
代码语言:javascript复制HeapOnly obj(*p2);//obj还是栈上的对象
通过 HeapOnly obj(*p2);
语句在栈上创建 HeapOnly
对象。这是因为 HeapOnly obj(*p2);
使用了拷贝构造函数,该构造函数是隐式定义的,并允许通过复制堆上的对象来创建栈上的对象。
解决这一问题,C 98中是将构造函数私有,并且只声明不实现:
HeapOnly obj(*p2);
这行代码会导致编译错误。虽然可以通过 CreateObj
在堆上创建对象,但由于拷贝构造函数没有定义,编译器无法进行对象的拷贝操作,因此这会引发错误。
C 11解决方式:在不希望调用的函数后面加上delete
拷贝构造函数:HeapOnly(const HeapOnly&) = delete;
删除了拷贝构造函数,禁止对象的拷贝操作。尝试复制 HeapOnly
对象会导致编译错误。
在C 标准库中,流对象不希望被拷贝:
继承和多态中的final
与override
关键字
模板的可变参数
C语言中的可变参数
C语言中也有可变参数的概念,他的底层是一个动态数组,存一个可变参数,然后一次解析动态可变参数。
C 中可变参数
C 中的可变参数不在函数中,而是在模板中体现。
一个基本可变参数的函数模板:
代码语言:javascript复制// Args是一个模板参数包,args是一个函数形参参数包
// 声明一个参数包Args...args,这个参数包中可以包含0到任意个模板参数。
template <class ...Args>
void ShowList(Args... args)
{}
递归函数方式展开参数包
编译时,参数推导递归:
代码语言:javascript复制void _ShowList()
{
cout << endl;
}
template<class T, class ...Args>
void _ShowList(const T& val, Args... args)
{
cout << val << " ";
_ShowList(args...);
}
template<class ...Args>
void ShowList(Args... args)
{
_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>
int PrintArg(T t)
{
cout << t << " ";
return 0;
}
//展开函数
template <class ...Args>
void ShowList(Args... args)
{
int arr[] = { (PrintArg(args)... }; //参数包里面有几个值,PrintArg函数就调用几次,就会有几个返回值,arr就会开多大
cout << endl;
}
int main()
{
ShowList(1);
ShowList(1, 'A');
ShowList(1, 'A', std::string("sort"));
return 0;
}
emplace容器系列
代码语言:javascript复制int main()
{
list<pair<gwj::string, int>> lt;
pair<gwj::string, int> kv1("xxxxx", 1);
lt.push_back(kv1);
lt.push_back(move(kv1));
cout << endl;
//pair<gwj::string, int> kv2("xxxxx", 1);
//lt.emplace_back(kv2);
//lt.emplace_back(move(kv2));
lt.emplace_back("xxxx", 1);
return 0;
}
对于push_back
需要传入pair
,左值走拷贝构造,右值走移动构造。但是,emplace_back
可以直接传入pair
对象的参数。