C++日新月异的未来代码:C++11(下)

接上篇,继续学习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 指针

🔥值得注意的是:

  1. 父作用域指包含 lambda 函数的语句块
  2. 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

  1. 语法上捕捉列表可由多个捕捉项组成,并以逗号分割,比如:[=, &a, &b],以引用传递的方式捕捉变量 ab,值传递方式捕捉其他所有变量;[&,a, this],值传递方式捕捉变量 athis,引用方式捕捉其他变量
  2. 捕捉列表不允许变量重复传递,否则就会导致编译错误
  3. 在块作用域以外的 lambda 函数捕捉列表必须为空,在全局作用域中,并没有局部变量可供 lambda 函数捕获
  4. 在块作用域中的 lambda 函数仅能捕捉父作用域中局部变量,捕捉任何非此作用域或者非局部变量都会导致编译报错
  5. 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函数模板实例化了三份,明明都是相同的内容,实在是没有必要,会导致模板的效率低下

那么这种时候就需要使用头文件 <functional> 中的 functionfunction 包装器也叫作适配器。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<void(int, int, int)> 与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 传过去

  • 若函数是静态成员函数,和普通函数一样都是全局函数,就不需要加 & 和传对象


希望读者们多多三连支持

小编会继续更新

你们的鼓励就是我前进的动力!

请添加图片描述

评论 62
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值