【C++】C++11的新特性 --- lambda表达式 ,新的类功能,模块的可变参数 , emplace系列接口

2024-07-29 08:24:55 浏览数 (2)

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表达式是一种匿名参数,格式是:

  1. [capture-list] : 捕捉列表,该列表总是出现在lambda函数的开始位置,编译器根据[ ]来判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量供lambda函数使用。可以省略。
  2. (parameters):参数列表。与普通函数的参数列表一致,如果不需要参数传递,则可以连同()一起省略
  3. mutable:默认情况下,lambda函数总是一个const函数,mutable可以取消其常量性。使用该修饰符时,参数列表不可省略(即使参数为空)。
  4. ->returntype:返回值类型。用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可省略。返回值类型明确情况下,一般都省略,由编译器对返回类型进行推导
  5. {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]是传值捕捉,类似传值参数,捕捉的是一个拷贝,当然是无法进行修改的!我们可以使用引用传参来达到修改的作用:

代码语言:javascript复制
	//传引用捕捉
	auto swap3 = [&a, &b]() mutable
		{
			int tmp = a;
			a = b;
			b = tmp;
		};
	swap3();

这样可以直接调用swap3做到交换的要求。但是这里回旋镖就回来了:[&a, &b]你说这是地址呢?还是引用呢? 设计引用时为了尽可能减少使用运算符,就使用了&!所以我们要注意[&a, &b]是引用方式捕捉!如果想要捕捉地址,就需要简介捕捉:

代码语言:javascript复制
int *pa = &a , *pb = &b;
auto swap3 = [pa, pb]()

这样就可以进行捕捉了!

来总结一下捕捉的种类:

  1. [var]:表示值传递方式捕捉变量var
  2. [=]:表示值传递方式捕获所有父作用域中的变量(包括this)
  3. [&var]:表示引用传递捕捉变量var
  4. [&]:表示引用传递捕捉所有父作用域中的变量(包括this)
  5. [this]:表示值传递方式捕捉当前的this指针
  6. 混合捕捉:[ 多种普通类型的捕捉 ]
代码语言:javascript复制
//全部捕捉
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 类中,有六个默认成员函数:

  1. 构造函数
  2. 析构函数
  3. 拷贝构造函数
  4. 拷贝赋值重载
  5. 取地址重载
  6. 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强制生成:

代码语言:javascript复制
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引用折叠可以接收右值也可以接收左值,那么拷贝构造就不会默认产生,所以进行强制生成后,要将四个进行绑定:

代码语言:javascript复制
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就不能随意构造

代码语言:javascript复制
class HeapOnly
{
public:
	HeapOnly* CreateObj()
	{
	}
private:
	HeapOnly()
	{
	}
	int _a = 1;
};

int main()
{
	HeapOnly* p = CreateObj();
}

这样就涉及“先有蛋,先有鸡”的问题,想要对象需要调用方法,想要方法需要对象。这是就可以将类对象方法使用static修饰,变为类方法,调用类域中的函数既可以

代码语言:javascript复制
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)
{}

之前使用的模版类中模版参数都是固定的,使用这个参数包就可以进行可变参数了!注意参数包的使用方式:

  1. template <class …Args>:...在前
  2. Args… args:后面是...

我们可以来看看是不是传入多个参数:

没问题,我们可以通过sizeof...(args)来检查传入了多少个参数。 那么如何调用参数包的数据呢?

代码语言:javascript复制
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是没有办法进行取出参数的,要进行递归逐个进行取用!同样的,我们也可以利用其他编译时方法来进行推导:数组处理。

代码语言:javascript复制
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的加强版!

代码语言:javascript复制
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对象的对象,传给参数包直接进行构造,实际上和移动构造的效率差距并不大,对于只会进行浅拷贝的类型就没有优化!

代码语言:javascript复制
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的底层,加入对应函数包的构造:

代码语言:javascript复制
template<class ...Args>
ListNode(Args&& ...args):
	_next(nullptr),
	_prev(nullptr),
	_data(args...)//参数包传到底层进行构造
{
}

再来细致来看看,_data的构造进行递归,如果是pair就直接进行了构造,如果是参数,就到pair的底层进行可变参数的构造!

传入一个pair就是生成一个const char* str , int val多参数函数

然后继续深入去调用!参数包的本质就是编译时实例化生成一个多参数的函数,使用多参数函数就是调用这个 多参数的函数!

Thanks♪(・ω・)ノ谢谢阅读!!!

下一篇文章见!!!

0 人点赞