[C++11#46](三) 详解lambda | 可变参数模板 | emplace_back | 默认的移动构造
目录
一.lambda
1. 捕捉列表
2. 底层原理
二. 可变参数模板
1. 递归函数方式展开参数包
2. 数组接收方式展开参数包
3. 运用
4.emplace_back
5.移动构造和拷贝构造
强制生成 default
一.lambda
可调用类的对象
- 函数指针--少用 void(*ptr) (int x)
- 仿函数--构造类 重载 operator() 对象可以像函数一样使用--eg.模板参数
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(), ComparePriceGreater());
}
- lambda--匿名函数对象 函数内部,直接定义使用
相当于一个局部的没有函数名的函数对象
int main()
{
auto compare = [](int x, int y) {return x > y; };
cout << compare(1, 2) << endl;//可以像函数一样调用
return 0;
}
内部可以调用全局函数吗?可以
局部函数呢?不可以
1. 捕捉列表
捕捉的方式
- [a,b] 传值捕捉
- [&a,&b] 传引用捕捉
- [=] 传值捕捉方式父作用域中所有变量(包括this指针)
- [&] 传引用捕捉方式父作用域中所有变量(包括this指针)
- 混合使用,例如捕捉[&x,y]
测试:
int main()
{
int a = 0, b = 1;
auto add1 = [](int x, int y) { return x + y; };
cout << add1(a, b) << endl;
auto add2 = [b](int x) {return x + b; };
cout << add2(a) << endl;
//引用的方式捕捉
auto swap = [&a, &b](){
int tmp = a;
a = b;
b = tmp;
};
swap();
cout << "After swapping: a = " << a << ", b = " << b << endl;
return 0;
}
特例:
- [=, &a, &b]:使用值传递方式捕获所有变量,除了
a
和b
,这两个变量将以引用方式捕获。 - [&,a, this]:使用引用传递方式捕获变量
a
和this
,其他变量将以值传递方式捕获。 - [=, a]:这个捕捉列表是错误的,因为
=
已经以值传递方式捕获了所有变量,而a
也被重复捕获了。 - 在块作用域以外的lambda函数:捕捉列表必须为空。这意味着lambda不能捕获任何外部变量。
- 在块作用域中的lambda函数:只能捕获父作用域中局部变量。
- lambda表达式之间不能相互赋值:即使看起来类型相同,也不能直接将一个lambda表达式赋值给另一个。这是因为lambda表达式有特定的捕获行为和生命周期,它们不能像普通变量那样直接赋值。
int main(){
int a = 0;
int b = 1;
int c = 2;
int d = 3;
const int e = 1;
cout << &e << endl;
// 引用的方式捕捉所有对象,除了a
// a用传值的方式捕捉
auto func = [&, a] {
//a++;
b++;
c++;
d++;
//e++;
cout << &e << endl;//const & 得到的是同一个地址
};
func();
return 0;
}
所以对于局部函数我们也可以捕捉后调用,但是没什么必要
2. 底层原理
UUID:唯一识别码
打印查看仿函数:
#include <iostream>
class Add {
public:
int operator()(int a, int b) const {
return a + b;
}
};
int main() {
Add add;
int result = add(10, 20);
std::cout << "Result: " << result << std::endl;
return 0;
}
底层:
将 lambda 和仿函数的底层对比查看:
int main()
{
auto f1 = [](int x, int y) {return x + y; };
auto f2 = [](int x, int y) {return x + y; };
//f1 = f2;
cout << typeid(f1).name() << endl;
cout << typeid(f2).name() << endl;
f1(1, 2);
return 0;
}
底层:
实际在底层编译器对于lambda表达式的处理方式,完全就是按照函数对象的方式处理的
- lambda_uuid(如上编译器没显示)
- 即:如果定义了一个lambda表达式,编译器会自动生成一个类,在该类中重载了operator()
编译器底层只有类和仿函数 operate()
二. 可变参数模板
例如 printf,想要几个参数就传几个
- 模板参数:类型
- 函数参数:对象
表示为 ...
//模板可变参数
template <class ...Args>
void CppPrint(Args... args)
{
cout << sizeof...(args) << endl;//打印参数个数
}
int main()
{
CppPrint();
CppPrint(1);
CppPrint(1, 2);
CppPrint(1, 2, 2.2);
CppPrint(1, 2, 2.2, string("xxxx"));
// ...
return 0;
}
我有一技: 可以对可变参数进行打印吗?不可以
类似实现 CppPrint:
要通过编译时的函数重载,递归推演来打印
1. 递归函数方式展开参数包
- 给函数模板增加一个模板参数,这样就可以从接收到的参数包中分离出一个参数出来
-
- 在函数模板中递归调用该函数模板,调用时传入剩下的参数包。
- 如此递归下去,每次分离出参数包中的一个参数,直到参数包中的所有参数都被取出来
- 还需要给一个递归终止函数
void _ShowList()
{
// 结束条件的函数
cout << endl;
}
template <class T, class ...Args>
void _ShowList(T val, Args... args)
{
cout << val << " ";
_ShowList(args...);
}
//args代表0-N的参数包
template <class ...Args>
void CppPrint(Args... args)
{
_ShowList(args...);
}
//传给_,递归挨个解析出第一个值
int main()
{
CppPrint(1);
CppPrint(1, 2);
CppPrint(1, 2, 2.2);
CppPrint(1, 2, 2.2, string("xxxx"));
// ...
return 0;
}
2. 数组接收方式展开参数包
还有一种新方式:利用编译器去推演,数组存储,可变参数包一个一个的取出来
- 这种展开参数包的方式,不需要通过递归终止函数,是直接在expand函数体中展开的, printarg不是一个递归终止函数,只是一个处理参数包中每一个参数的函数
template <class T>
void PrintArg(T t)
{
cout << t << " ";
}
// 参数包有几个值,就展开调用几次
template <class... Args>
void ShowList(Args... args)
{
int arr[] = {PrintArg(args)...};//利用编译器去推演,数组存储
cout << endl;
}
int main()
{
ShowList(1);
ShowList(1, 'A');
ShowList(1, 'A', std::string("sort"));
return 0;
}
3. 运用
模板的可变参数实现调用的多样化,灵活
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
:_year(year)
,_month(month)
,_day(day)
{
cout << "Date构造" << endl;
}
Date(const Date& d)
:_year(d._year)
, _month(d._month)
, _day(d._day)
{
cout << "Date拷贝构造" << endl;
}
private:
int _year;
int _month;
int _day;
};
template <class ...Args>
Date* Create(Args... args)
{
Date* ret = new Date(args...);
return ret;
}
int main()
{
Date* p1 = Create();
Date* p2 = Create(2023);
Date* p3 = Create(2023, 9);
Date* p4 = Create(2023, 9, 27);
//拷贝构造
Date d(2023, 1, 1);
Date* p5 = Create(d);
return 0;
}
4.emplace_back
带模板参数的&&是万能引用,结合可变参数
emplace 效果的体现场景:主要体现在浅拷贝的拷贝构造的优化
- emplace 能一直往下传
- push_back 是先构造,再拷贝构造/移动构造
emplace 可以理解为一个优化的 push_back
5.移动构造和拷贝构造
新的类功能:默认成员函数
在原来的C++类中,编译器会默认生成六个默认成员函数:
- 构造函数
- 析构函数
- 拷贝构造函数
- 拷贝赋值运算符重载
- 取地址运算符重载
const
取地址运算符重载
其中,最为常用的是前四个,后两个通常使用较少。这些默认成员函数在我们没有显式定义时,编译器会自动生成。
C++11 新增了两个默认成员函数:
- 移动构造函数
- 移动赋值运算符重载
移动构造函数和移动赋值运算符重载的注意点
- 如果没有自己定义移动构造函数,并且没有实现析构函数、拷贝构造函数或拷贝赋值运算符中的任意一个,那么编译器会自动生成一个默认的移动构造函数。
- 对于内置类型成员,执行逐成员按字节的浅拷贝。
- 对于自定义类型成员,若该成员实现了移动构造函数,则调用移动构造函数;若没有,则调用拷贝构造函数。
- 类似地,如果没有定义移动赋值运算符重载函数,如上
- 一旦提供了移动构造函数或移动赋值运算符,编译器就不会再自动生成拷贝构造函数和拷贝赋值运算符。
class Person
{
public:
Person(const char* name = "", int age = 0)
: _name(name), _age(age) {}
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;
}
在上述代码中,由于没有定义析构函数、拷贝构造函数和拷贝赋值运算符,编译器会自动生成默认的移动构造和移动赋值运算符。
为什么编译器会自动生成默认的移动构造和移动赋值?
通常,对于需要深拷贝或需要释放资源的类,开发者会显式定义拷贝构造函数、赋值运算符重载和析构函数。如果没有显式定义这些函数,编译器生成的默认移动构造函数和移动赋值运算符将对内置类型执行值拷贝
强制生成 default
在C++11中,可以使用default
关键字来强制生成某些默认成员函数,即使开发者提供了其他构造函数。例如,提供了拷贝构造函数后,编译器不会再生成默认的移动构造函数,但可以通过= default
来显式请求生成。
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:
bit::string _name;
int _age;
};
即使手动编写了拷贝构造函数,仍然可以通过= default
来让编译器生成移动构造函数。