1 lambda表达式
1.1 基本用法
C语言解决自定义排序问题时,会使用函数指针;C 我们解决排序问题时,一般都会使用仿函数,通过自定义类来实现自定义比较大小。如果涉及的比较排序很多,就要写出很多类,比较繁琐。通 今天的lambda表达式也是一种解决办法。我们来看:这是我们传统的仿函数写法
代码语言:javascript复制struct Goods
{
string _name; // 名字
double _price; // 价格
int _evaluate; // 评价
Goods(const char* str, double price, int evaluate)
:_name(str)
, _price(price)
, _evaluate(evaluate)
{}
};
class GoodsPriceLess
{
public:
bool operator()(Goods& a , Goods& b)
{
return a._price > b._price;
}
};
class GoodsPriceGreater
{
public:
bool operator()(Goods& a, Goods& b)
{
return a._price < b._price;
}
};
int main()
{
vector<Goods> v = { { "苹果", 2.1, 5 }, { "香蕉", 3, 4 }, { "橙子", 2.2,3 }, { "菠萝", 1.5, 4 } };
sort(v.begin(), v.end(), GoodsPriceLess());
for (auto g : v)
{
cout << g._name << " " << g._price << " " << g._evaluate << endl;
}
sort(v.begin(), v.end(), GoodsPriceGreater());
for (auto g : v)
{
cout << g._name << " " << g._price << " " << g._evaluate << endl;
}
return 0;
}
通过lambda表达式可以简单化:
代码语言:javascript复制int main()
{
vector<Goods> v = { { "苹果", 2.1, 5 }, { "香蕉", 3, 4 }, { "橙子", 2.2,3 }, { "菠萝", 1.5, 4 } };
sort(v.begin(), v.end(), [](const Goods& a, const Goods& b) { return a._price > b._price; });
for (auto g : v)
{
cout << g._name << " " << g._price << " " << g._evaluate << endl;
}
cout << endl;
sort(v.begin(), v.end(), [](const Goods& a, const Goods& b) { return a._price < b._price; });
for (auto g : v)
{
cout << g._name << " " << g._price << " " << g._evaluate << endl;
}
return 0;
}
lambda表达式是一种匿名参数,格式是:
[capture-list]
: 捕捉列表,该列表总是出现在lambda函数的开始位置,编译器根据[ ]来判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量供lambda函数使用。可以省略。(parameters)
:参数列表。与普通函数的参数列表一致,如果不需要参数传递,则可以连同()一起省略mutable
:默认情况下,lambda函数总是一个const函数,mutable可以取消其常量性。使用该修饰符时,参数列表不可省略(即使参数为空)。->returntype
:返回值类型。用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可省略。返回值类型明确情况下,一般都省略,由编译器对返回类型进行推导。{statement}
:函数体。在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量
可以使用auto 来承接这个匿名函数
代码语言:javascript复制auto func1 = [](int a, int b) { return a b; }
就可以调用func1
来做到像函数一样的效果!
我们可以看一下这个匿名函数的类型:
即使是一模一样的,类型也是不同的!
lambda的本质是仿函数,类型是lambda 一个随机字符串UUID
,也就是一个仿函数的名称,编译器在编译时,会生成对应仿函数的名称。lambda表达式就类似范围for
,只是表现不同,底层本质还是仿函数!
1.2 细谈参数列表与捕捉列表
我们来看一个程序:
代码语言:javascript复制int main()
{
int a = 1 ; int b = 2;
auto swap1 = [](int& x, int& y)
{
int tmp = x;
x = y;
y = tmp;
};
swap1(a, b);
return 0;
}
这个程序可以帮我们完成交换a、b的值。注意这里使用auto可以帮助我们解决不知道函数类型的问题。同样我们也可以不通过参数列表来达到更换的作用:
代码语言:javascript复制 //捕捉a b 对象给lambda表达式用
//注意加上mutable才能对捕捉对象进行修改(一般不需要)
auto swap2 = [a, b]() mutable
{
int tmp = a;
a = b;
b = tmp;
};
swap2();
通过调试,我们发现swap2函数并没有对ab进行交换,只是在函数作用域下进行了交换,因为[a, b]
是传值捕捉,类似传值参数,捕捉的是一个拷贝,当然是无法进行修改的!我们可以使用引用传参来达到修改的作用:
//传引用捕捉
auto swap3 = [&a, &b]() mutable
{
int tmp = a;
a = b;
b = tmp;
};
swap3();
这样可以直接调用swap3做到交换的要求。但是这里回旋镖就回来了:[&a, &b]
你说这是地址呢?还是引用呢?
设计引用时为了尽可能减少使用运算符,就使用了&
!所以我们要注意[&a, &b]
是引用方式捕捉!如果想要捕捉地址,就需要简介捕捉:
int *pa = &a , *pb = &b;
auto swap3 = [pa, pb]()
这样就可以进行捕捉了!
来总结一下捕捉的种类:
- [var]:表示值传递方式捕捉变量var
- [=]:表示值传递方式捕获所有父作用域中的变量(包括this)
- [&var]:表示引用传递捕捉变量var
- [&]:表示引用传递捕捉所有父作用域中的变量(包括this)
- [this]:表示值传递方式捕捉当前的this指针
- 混合捕捉:[ 多种普通类型的捕捉 ]
//全部捕捉
int a = 1, b = 2, c = 4, d = 5;
auto swap4 = [=]() mutable
{
return a b * c - d;
};
int ret = swap4();
好的,lambda表达式就是这些内容,一定要运用到实际中去,在一些需要仿函数的地方可以进行lambda表达式的优化!
2 新的类功能
2.1 移动构造与移动赋值
在原本的C 类中,有六个默认成员函数:
- 构造函数
- 析构函数
- 拷贝构造函数
- 拷贝赋值重载
- 取地址重载
- const 取地址重载
最后重要的是前4个,后两个用处不大。默认成员函数就是我们不写编译器会生成一个默认的! 注意只有写了任意一个构造函数(构造,拷贝构造,拷贝赋值)就不生成默认构造
在C 11之后,加入了右值引用和移动语义,就产生了新的类默认成员函数—移动构造和移动赋值。这些针对的是需要深拷贝的自定义类型(string , vector,list)通过与将亡值的数据进行交换做到效率提高!
针对移动构造函数和移动赋值运算符重载有一些需要注意的点如下:
如果你没有自己实现移动构造函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个。那么编译器会自动生成一个默认移动构造。析构函数 、拷贝构造、拷贝赋值通常是绑定在一起的(需要深拷贝),实现一个就都要写,否则一个不写!默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造。
代码语言:javascript复制class Person
{
public:
Person(const char* name = "", int age = 0)
:_name(name)
, _age(age)
{}
private:
bit::string _name;
int _age;
};
int main()
{
Person s1; //string会进行一次构造
Person s2 = s1;//
Person s3 = std::move(s1);
Person s4;
s4 = std::move(s2);
return 0;
}
Person s2 = s1;
因为没有写拷贝构造,所以会默认生成一个,内置类型逐字节拷贝,自定义类型如果有拷贝构造就调用拷贝构造否则进行浅拷贝。
Person s3 = std::move(s1);
move后是右值,会进行移动构造!s1和s3交换数据s4 = std::move(s2);
因为s4已经构造过,进行移动赋值!s2和s4交换数据
如果我们在person
内部加入析构函数 、拷贝构造、拷贝赋值
任意一个,就不会产生默认构造了
如果你没有自己实现移动赋值重载函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个,那么编译器会自动生成一个默认移动赋值。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。(默认移动赋值跟上面移动构造完全类似)
如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值。
2.2 default和delete
default
关键字可以强制生成!如果我们写了析构函数 、拷贝构造、拷贝赋值重载中几个,我们还想要生成默认移动构造,就可以使用default强制生成:
class Person
{
public:
Person(const char* name = "", int age = 0)
:_name(name)
, _age(age)
{}
//强制生成
Person(Person&& p) = default;
~Person()
{
}
private:
bit::string _name;
int _age;
};
但是会发生报错:
因为加入Person(Person&& p)
会产生一系列受到影响,Person&& p
引用折叠可以接收右值也可以接收左值,那么拷贝构造就不会默认产生,所以进行强制生成后,要将四个进行绑定:
public:
Person(const char* name = "", int age = 0)
:_name(name)
, _age(age)
{}
//强制生成
Person(const Person& p) = default;
Person& operator=(const Person& p) = default;
Person(Person&& p) = default;
Person& operator=(Person&& p) = default;
~Person()
{
}
private:
bit::string _name;
int _age;
};
禁止生成默认函数的关键字delete:
如果能想要限制某些默认函数的生成,在C 98中,是该函数设置成private,并且只声明不定义,这样只要其他人想要调用就会报错。在C 11中更简单,只需在该函数声明加上=delete即可,该语法指示编译器不生成对应函数的默认版本,称=delete
修饰的函数为删除函数。:
- C 98 : 私有 只声明不实现
- C 11 : 直接delete
遇到不想要进行拷贝的类可以使用delete,例如单例模式下的对象,只希望产生一个对象。
或者需要类的对象只能生成在堆上,就可以将构造函数delete
就不能随意构造
class HeapOnly
{
public:
HeapOnly* CreateObj()
{
}
private:
HeapOnly()
{
}
int _a = 1;
};
int main()
{
HeapOnly* p = CreateObj();
}
这样就涉及“先有蛋,先有鸡”的问题,想要对象需要调用方法,想要方法需要对象。这是就可以将类对象方法使用static
修饰,变为类方法,调用类域中的函数既可以
HeapOnly* p = HeapOnly::CreateObj();
但是这样没有把路子卡死:
代码语言:javascript复制HeapOnly obj(*p);
就又可以进行栈上对象的拷贝构造了,所以不期望进行一个拷贝,就要将拷贝构造进行delete! 流对象就是不可以进行拷贝的
3 模块的可变参数
可变参数在C语言中我们见过的:
其中的…就是可变参数,我们可以传入任意的参数,都可以进行按照格式进行打印,这个的底层是一个数组,通过这个数组来获取所有的参数。虽然printf可以传入多个参数,但是只能打印内置类型!而通过模版的可变参数可以打印任意类型!
在C 中的可变参数上升了一个维度:模版的可变参数
代码语言:javascript复制// Args是一个模板参数包,args是一个函数形参参数包
// 声明一个参数包Args...args,这个参数包中可以包含0到任意个模板参数。
template <class ...Args>
void ShowList(Args... args)
{}
之前使用的模版类中模版参数都是固定的,使用这个参数包就可以进行可变参数了!注意参数包的使用方式:
- template <class …Args>:
...
在前 - Args… args:后面是
...
我们可以来看看是不是传入多个参数:
没问题,我们可以通过sizeof...(args)
来检查传入了多少个参数。
那么如何调用参数包的数据呢?
for(int i = 0 ; i < sizeof...(args) ; i )
{
cout<< args[i] <<endl;
}
注意奥,不是数组奥!这里不是通过数组来实现的!上面是运行时代码,但实际上解析模版参数包的工作是编译时做的!一定要区分好编译时和运行时。所以是不可能支持怎么操作的! 实际上是使用递归来做到:
代码语言:javascript复制//终止递归函数
template<class T>
void _PrintList(const T& val)
{
cout << val << endl;
}
//过程函数
template<class T ,class ...Args>
void _PrintList(const T& val ,Args... args)
{
cout << val << " ";
_PrintList(args...);
}
//起始函数
template<class ...Args>
void PrintList(Args... args)
{
_PrintList(args...);
}
这样就可以进行一个打印了!在编译时,编译器会自动推导出来这个打印函数的参数:(以三个参数的为例)
直接对...args
是没有办法进行取出参数的,要进行递归逐个进行取用!同样的,我们也可以利用其他编译时方法来进行推导:数组处理。
template<class T>
int _PrintList(const T& val)
{
cout << val << endl;
return 0;
}
template<class ...Args>
void PrintList(Args... args)
{
int arr[] = { _PrintList(args)... };
}
通过这个数组会在编译器里进行推导!args...
里面有几个参数,就会调用_PrinfList多少次
,就会有几个返回值!
4 emplace系列接口
我们来看emplace系列接口:
在这里就使用到了模版的可变参数,是push_back
的加强版!
int main()
{
std::vector< std::pair<int, char> > v;
// emplace_back支持可变参数,拿到构建pair对象的参数后自己去创建对象
// 那么在这里我们可以看到除了用法上,和push_back没什么太大的区别
v.emplace_back(10, 'a');
v.emplace_back(make_pair(20, 'b'));
std::pair<int, char> p(30, 'c');
v.push_back(p);
for (auto e : v)
cout << e.first << ":" << e.second << endl;
return 0;
}
运行看看:
插入bc
时是和push_back是一致的 !但是比较特殊的是a
,直接进行可变参数的插入!只要是单个参数是没有区别的,v.emplace_back(10, 'a');
多参数的构造是emplace的优势!构造pair对象的对象,传给参数包直接进行构造,实际上和移动构造的效率差距并不大,对于只会进行浅拷贝的类型就没有优化!
template<class ...Args>
void emplace_back(Args&&... args)
{
//进行完美转发,避免将右值变成左值
emplace(end() , forward<T>(args)...);
}
//emplace
template<class ...Args>
void emplace(Args&&... args)
{
Node* prev = pos._node->_prev;
Node* next = pos._node;
//其余都是一样的
//这里直接传入参数包
//需要修改底层
Node* node = new Node(args...);
node->_prev = prev;
node->_next = next;
prev->_next = node;
next->_prev = node;
_size ;
}
在list
的上层加入了emplace,还有需要修改Node
的底层,加入对应函数包的构造:
template<class ...Args>
ListNode(Args&& ...args):
_next(nullptr),
_prev(nullptr),
_data(args...)//参数包传到底层进行构造
{
}
再来细致来看看,_data
的构造进行递归,如果是pair就直接进行了构造,如果是参数,就到pair的底层进行可变参数的构造!
传入一个pair
就是生成一个const char* str , int val
多参数函数
然后继续深入去调用!参数包的本质就是编译时实例化生成一个多参数的函数,使用多参数函数就是调用这个 多参数的函数!