C++日新月异的未来代码:C++11(下)
文章目录
- 1.lambda表达式
- 1.1 引入
- 1.2 语法
- 1.3 使用
- 1.4 本质
- 2.类的新增语法
- 2.1 移动构造、移动赋值运算符
- 2.2 default
- 2.3 delete
- 3.可变参数模板
- 3.1 概念
- 3.2 获取个数
- 3.3 展开参数包
- 3.3.1 递归函数
- 3.3.2 逗号表达式
- 3.4 emplace系列的接口
- 3.5 可变参数模板的实际应用
- 4.包装器
- 4.1 function
- 4.2 bind
- 希望读者们多多三连支持
- 小编会继续更新
- 你们的鼓励就是我前进的动力!
接上篇,继续学习C++11的常用新特性
1.lambda表达式
1.1 引入
lambda
表达式是一种匿名函数对象,允许在代码中直接定义和使用小型的函数,无需额外定义函数或函数对象类,这么讲,感觉还是太理论了,下面将通过特定场景介绍其使用:
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());
}
日常生活中,一件商品包含多个特性,若想针对某个特性进行排序,那么就需要使用算法库里的 sort
,设置自定义类型的比较方式,那么仿函数就是个很好的方式
随着 C++
语法的发展,人们开始觉得上面的写法太复杂了,每次为了实现一个 algorithm
算法,都要重新去写一个类,如果每次比较的逻辑不一样,还要去实现多个类,特别是相同类的命名,这些都给编程者带来了极大的不便。因此,在 C++11
语法中出现了 lambda
表达式
1.2 语法
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
函数不能做任何事情
1.3 使用
int main()
{
vector<Goods> v = { { "苹果", 2.1, 5 }, { "香蕉", 3, 4 }, { "橙子", 2.2,
3 }, { "菠萝", 1.5, 4 } };
sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2)->bool {
return g1._price < g2._price; });
sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2)->bool {
return g1._price > g2._price; });
sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2)->bool {
return g1._evaluate < g2._evaluate; });
sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2)->bool {
return g1._evaluate > g2._evaluate; });
return 0;
}
因此,lambda
表达式可以这样套用在 sort
里,比仿函数确实方便且可观性更高了,可以看出 lambda
表达式实际是一个匿名函数(无名函数),该函数无法直接调用,如果想要直接调用,可借助 auto
将其赋值给一个变量
auto ret
= [ ](const Goods& g1
,const Goods& g2
) {return g1._evaluate
<g2._evaluate
; }
对于捕捉列表 []
,平常一般使用的不多,但是某些情况还是要使用的,需要了解其用法
捕捉列表描述了上下文中那些数据可以被lambda使用,以及使用的方式传值还是传引用:
[x]
:表示值传递
方式捕捉变量x
[=]
:表示值传递
方式捕获所有父作用域中的变量(包括this
)[&x]
:表示引用传递
捕捉变量x
[&]
:表示引用传递
捕捉所有父作用域中的变量(包括this
)[this]
:表示值传递
方式捕捉当前的this
指针
🔥值得注意的是:
- 父作用域指包含
lambda
函数的语句块 lambda
默认以值传递的方式进行,传值捕捉的变量是不可修改的
int main()
{
int x = 10;
auto func = [x]() mutable { x = 20; cout << x << std::endl; };
func();
cout << x << endl;
return 0;
}
使用 mutable
关键字就可以修改了,但是这种修改只是对 lambda
内部的副本进行修改,不会影响到原始的变量。在 main
函数中再次输出 x
时,其值仍为 10
- 语法上捕捉列表可由多个捕捉项组成,并以逗号分割,比如:
[=, &a, &b]
,以引用传递的方式捕捉变量a
和b
,值传递方式捕捉其他所有变量;[&,a, this]
,值传递方式捕捉变量a
和this
,引用方式捕捉其他变量 - 捕捉列表不允许变量重复传递,否则就会导致编译错误
- 在块作用域以外的
lambda
函数捕捉列表必须为空,在全局作用域中,并没有局部变量可供lambda
函数捕获 - 在块作用域中的
lambda
函数仅能捕捉父作用域中局部变量,捕捉任何非此作用域或者非局部变量都会导致编译报错 lambda
表达式之间不能相互赋值,即使看起来类型相同,但是可以拷贝构造(每个lambda
表达式都有其独特的、未命名的类型。即使两个lambda
表达式的参数列表和返回类型相同,它们的类型也是不同的)
1.4 本质
转到反汇编可以发现,其实 lambda
的本质就是被包装的仿函数,编译器会自动生成一个类,在该类中重载了 operator()
2.类的新增语法
2.1 移动构造、移动赋值运算符
C++11
新增了两个:移动构造
函数和移动赋值运算符
重载,在上一篇有进行详细的说明
传送门:C++日新月异的未来代码:C++11(上)
-
如果你没有自己实现移动构造函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个。那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造
-
如果你没有自己实现移动赋值重载函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个,那么编译器会自动生成一个默认移动赋值。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。(默认移动赋值跟上面移动构造完全类似)
-
如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值
2.2 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;
};
int main()
{
Person s1;
Person s2 = s1;
Person s3 = std::move(s1);
return 0;
}
default
是强制生成默认函数的关键字,我们提供了拷贝构造,就不会生成移动构造了,那么我们可以使用 default
关键字显示指定移动构造生成
2.3 delete
class Person
{
public:
Person(const char* name = "", int age = 0)
:_name(name)
, _age(age)
{}
Person(const Person& p) = delete;
private:
bit::string _name;
int _age;
};
int main()
{
Person s1;
Person s2 = s1;
Person s3 = std::move(s1);
return 0;
}
delete
是禁止生成默认函数的关键字,当类显式删除了拷贝构造函数时,编译器不会自动生成移动构造函数(即使没有显式删除移动构造函数),代码中没有显式定义移动构造函数,且隐式移动构造函数被禁用,因此无法完成移动初始化
🔥值得注意的是:
移动构造函数的核心目的是高效转移资源所有权(如动态内存、文件句柄等),而拷贝构造函数的目的是创建资源的独立副本。如果一个类禁用了拷贝构造函数,通常意味着:
- 资源不可复制: 例如独占式资源,拷贝会导致资源管理混乱
- 防止意外拷贝: 开发者希望禁止对象的复制操作,强制使用移动语义
此时,如果编译器仍然自动生成移动构造函数,可能会破坏这种设计意图
3.可变参数模板
3.1 概念
其实可变模板参数早在C语言就已经有了,后面三个点点点就是可变模板参数,比如: printf("%d,%d,%d", x, y, z)
,后面的参数个数是可以自己控制有多少个的,这就是一种可变模板参数
template <class ...Args>
void ShowList(Args... args)
{}
int main()
{
ShowList();
ShowList(1);
ShowList(1, 2.2);
ShowList(1, 2, "xxxxx");
return 0;
}
回到实际定义,Args
是一个模板参数包,args
是一个函数形参参数包,声明一个参数包Args... args
,这个参数包中可以包含 0
到任意个模板参数
3.2 获取个数
template <class ...Args>
void ShowList(Args... args)
{
cout << sizeof...(args) << endl;
}
int main()
{
ShowList();
ShowList(1);
ShowList(1, 2.2);
ShowList(1, 2, "xxxxx");
return 0;
}
这个用法也是很奇葩。。。
3.3 展开参数包
不知道当初设计怎么想的,这里想要 for
循环遍历展开是不可行的,编译器不支持,所以这里的展开方法做了解即可
3.3.1 递归函数
// 递归终止函数
template <class T>
void ShowList(const T& t)
{
cout << t << 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', string("sort"));
return 0;
}
模式匹配: 展开函数 ShowList(T value, Args... args)
匹配 至少一个参数 的情况,每次取出第一个参数 value
,剩余参数构成新的参数包 args...
,终止函数 ShowList(const T& t)
匹配仅有一个参数 的情况,结束递归
参数包展开: args...
在递归调用时会被解包,每次减少一个参数,直到参数包为空,
关键语句 ShowList(args...)
会触发模板的递归实例化,直到匹配终止函数
输出顺序: 先打印当前参数 value
,再递归处理剩余参数,确保参数按传入顺序输出
3.3.2 逗号表达式
template <class T>
void PrintArg(T t)
{
cout << t << " ";
}
//展开函数
template <class ...Args>
void ShowList(Args... args)
{
int arr[] = { (PrintArg(args), 0)... };
cout << endl;
}
int main()
{
ShowList(1);
ShowList(1, 'A');
ShowList(1, 'A', string("sort"));
return 0;
}
(PrintArg(args), 0)...
会将参数包 args...
展开为多个表达式,打印对应的值,然后返回 0
(用于填充数组)
// 原始代码
int arr[] = { (PrintArg(args), 0)... };
// 展开后等价于
int arr[] = { (PrintArg(1), 0), (PrintArg('A'), 0), (PrintArg("sort"), 0) };
PrintArg
的返回值是 void
,无法初始化 int
数组,即使 PrintArg
返回参数类型(如 T
),参数包可能包含不同类型(如 int
, char
),仍会导致类型不匹配
每个元素必须是 int
类型,因此需要用 0
作为统一的返回值,保证初始化的数组元素都为相同类型
3.4 emplace系列的接口
int main()
{
list< pair<int, char> > mylist;
mylist.emplace_back(10, 'a');
mylist.emplace_back(20, 'b');
mylist.emplace_back(make_pair(30, 'c'));
mylist.push_back(make_pair(40, 'd'));
mylist.push_back({ 50, 'e' });
for (auto e : mylist)
cout << e.first << ":" << e.second << endl;
return 0;
}
emplace_back
的作用和 push_back
相同,但是 mylist.emplace_back(20, 'b')
这种格式的写法更方便一些
其实我们会发现其实差别也不大,emplace_back
是直接构造了,push_back
是先构造,再移动构造,移动构造的消耗很小,其实没啥影响
3.5 可变参数模板的实际应用
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()
{
list<Date> lt;
Date d(2023, 9, 27);
// 只能传日期类对象
lt.push_back(d);
// 既能传日期类对象
// 又能传日期类对象的参数包
// 参数包,一路往下传,直接去构造或者拷贝构造节点中日期类对象
lt.emplace_back(d);
lt.emplace_back(2023, 9, 27);
return 0;
}
push_back
只能传日期类对象,emplace_back
既能传日期类对象,又能传日期类对象的参数包。参数包,一路往下传,直接去构造或者拷贝构造节点中日期类对象
4.包装器
4.1 function
template<class F, class T>
T useF(F f, T x)
{
static int count = 0;
cout << "count:" << ++count << endl;
cout << "count:" << &count << endl;
return f(x);
}
double f(double i)
{
return i / 2;
}
struct Functor
{
double operator()(double d)
{
return d / 3;
}
};
int main()
{
// 函数名
cout << useF(f, 11.11) << endl;
// 函数对象
cout << useF(Functor(), 11.11) << endl;
// lamber表达式
cout << useF([](double d)->double { return d / 4; }, 11.11) << endl;
return 0;
}
我们知道函数指针,仿函数,lambda表达式,这三种都是函数对象的创建方式,同时调用这三个方式实例化模板,useF函数模板实例化了三份,明明都是相同的内容,实在是没有必要,会导致模板的效率低下
那么这种时候就需要使用头文件
中的 function
,function
包装器也叫作适配器。C++中的 function
本质是一个类模板,也是一个包装器
// 类模板原型如下
template <class T> function; // undefined
template <class Ret, class... Args>
class function<Ret(Args...)>;
模板参数说明:
Ret
: 被调用函数的返回类型Args…
:被调用函数的形参
下面直接修改以上代码,来展示 function
的使用效果:
template<class F, class T>
T useF(F f, T x)
{
static int count = 0;
cout << "count:" << ++count << endl;
cout << "count:" << &count << endl;
return f(x);
}
double f(double i)
{
return i / 2;
}
struct Functor
{
double operator()(double d)
{
return d / 3;
}
};
int main()
{
// 函数名
function<double(double)> func1 = f;
cout << useF(func1, 11.11) << endl;
// 函数对象
function<double(double)> func2 = Functor();
cout << useF(func2, 11.11) << endl;
// lamber表达式
function<double(double)> func3 = [](double d)->double { return d / 4; };
cout << useF(func3, 11.11) << endl;
return 0;
}
三种可调用对象被统一为同一类型:包装类,模板只实例化一次,静态变量共享(即这个 count
只有一份),
4.2 bind
bind
函数定义在头文件中,是一个函数模板,它就像一个函数包装器(适配器),接受一个可调用对象,生成一个新的可调用对象来“适应”原对象的参数列表。
bind
可以理解为一个接收包装类的适配器,上面的例子都是直接将函数给到包装类,那么 bind
就是将特定的函数和参数绑定到包装类,通过例子解析会更容易理解:
int Plus(int a, int b)
{
return a + b;
}
class Sub
{
public:
int sub(int a, int b)
{
return a - b;
}
};
int main()
{
//表示绑定函数plus 参数分别由调用 func1 的第一,二个参数指定
std::function<int(int, int)> func1 = std::bind(Plus, placeholders::_1,
placeholders::_2);
//auto func1 = std::bind(Plus, placeholders::_1, placeholders::_2);
//func2的类型为 function 与func1类型一样
//表示绑定函数 plus 的第一,二为: 1, 2
auto func2 = std::bind(Plus, 1, 2);
cout << func1(1, 2) << endl;
cout << func2() << endl;
Sub s;
// 绑定成员函数
std::function<int(int, int)> func3 = std::bind(&Sub::sub, s,
placeholders::_1, placeholders::_2);
// 参数调换顺序
std::function<int(int, int)> func4 = std::bind(&Sub::sub, s,
placeholders::_2, placeholders::_1);
cout << func3(1, 2) << endl;
cout << func4(1, 2) << endl;
return 0;
}
bind
的第一个参数传的是函数,后面的是一系列要传的参数,_1
为第一个参数,_2
为第二个参数,以此类推,参数既可以是待定的,也可以是具体的值,placeholders
属于 std
命名空间,若展开了就不用写
🔥值得注意的是:
-
若函数是非静态成员函数,必须在
Sub::sub
前加上&
,因为非静态成员函数依赖对象,必须显式调用其地址,普通函数指针直接指向代码地址,而成员函数指针需要同时包含类的类型信息和函数地址,因此还需要将对象s
传过去 -
若函数是静态成员函数,和普通函数一样都是全局函数,就不需要加
&
和传对象