C++11-(3)

发布于:2025-05-24 ⋅ 阅读:(14) ⋅ 点赞:(0)

(一)C++11新增功能

1.1可变模板参数

1.1.1 基本语法及原理

C++11支持可变参数模板,也就是说支持可变数量参数的函数模板和类模板,可变数目的参数被称为参数包,存在两种参数包:模板参数包,表示零个或多个模板参数;函数参数包,表示零个或多个函数参数。printf这个函数的实现就是使用了可变参数模板

格式如下:

template<class ...Args> void func(Args... args){ }
template<class ...Args> void func(Args&... args){ } //左值引用
template<class ...Args> void func(Args&&... args){ } //右值引用(万能引用)

格式中“…Args”和“…args”分别表示模板参数的参数包和函数参数的参数包,参数包可以是零个或者多个。在模板参数列表中,class…或typename…指出接下来的参数表示零个或多个类型列表;在函数参数列表中,类型名后面…指出接下来表示零个或多个形象列表;函数参数包可以用左值引用或右值引用表示,每个参数实例化遵循引用折叠的规则

可变参数模板的原理跟模板类似,本质还是去实例化对应类型和个数的多个函数,其中sizeof…()可以计算参数包的个数

使用举例:

#include <iostream>
template<class ...Args>
void Print(Args ...args)
{
	std::cout << sizeof...(args) << std::endl;
}

int main()
{
	Print(); //参数包为0
	Print(1); //参数包为1
	Print(11, std::string("11111")); //参数包为2
	double x = 1.1;
	Print(1, std::string("2222"), x); //参数包为3
	return 0;
}

在这里插入图片描述
该模板的原理:

通过上面举例的代码来探讨它的原理

  1. 上面的代码表面上看起来调用的是同一个函数,但实际上编译器会结合引用折叠规则实例化出四个函数分别是,void Print(); void Print(int&& arg1); void Print(int&& arg1,string&& arg2); Print(int&& agr1,string&& arg2,double& agr3);
  2. 再继续往下理解,如果没有可变参数模板,我们要实现多个这样的函数模板才能支持传不同的参数个数这样的功能,有了可变参数模板,编译器就会根据实参类型示例话出对应的类,如下:
void print();

template<class T1>
void Print(T1&& agr1);

template<class T1,class T2>
void Print(T1&& agr1,T2&& arg2);

template<class T1,class T2,class T3>
void Print(T1&& arg1,T2&& arg2,T3& arg3);

总结:
普通函数模板:针对类型
本来要写多个函数->只用写一个函数模板即可

可变参数模板:针对数量
本来要写多个普通函数模板->只用写一个可变参数模板即可

1.1.2 包扩展

扩展包,相当于将参数包里面的参数一个个的取出来

  • 获取方式一:递归推演
#include <iostream>
void showlist()
{
	//当参数包为0时,调用用该函数进行终止
	std::cout << std::endl;
}

template<class T,class ...Args>
void showlist(T x, Args ...args)
{
	std::cout << x << " ";
	
	//args是一个有N个参数的参数包
	//调用showlist时,将第一个参数传给x,将后面的N-1个参数传给...args
	showlist(args...);
}

//编译时,递归推导解析参数
template<class ...Args>
void Print(Args ... args)
{
	showlist(args...);
}

int main()
{
	Print(1);
	Print(1, "xxxxx");
	double x = 1.1;
	Print(2, "aaaa", x);
	return 0;
}

在这里插入图片描述
上面的showlist函数看起来是一个,实际上编译器会对传过来的参数包进行递归推导,依次解析参数包里面的内容,然后实例化出多个函数,然后在多个函数包里面,再依次取出参数包的内容,再继续递归,该过程就叫包扩展

  • 获取方式二:
#include <iostream>
template<class T>
const T& GetArg(const T& x)
{
	std::cout << x << " ";
	return x;
}
template<class ...Args>
void Arguments(Args ...args)
{
	std::cout << std::endl;
}
template<class ...Args>
void Print(Args ...args)
{
	Arguments(GetArg(args)...);
}
int main()
{
	double x = 1.1;
	Print(2, "aaaa", x);
	return 0;
}

在这里插入图片描述
该方式本质还是与方式一完全类似,就是套了层“壳”,相当于将获取到的参数包的第一个参数进行加工,也就是打印出来,再传回去。注意,GetArg必须返回获得到的对象,这样才能组成参数包传给Arguments,该方式是对GetArg进行了展开

一般情况下,不会使用上面两种方式来进行获取,而是通过直接匹配的方式,继续往下看。

1.1.3 emplace系列接口

在C++11以后,很多容器都增加了emplace系列的接口,以list容器为例
在这里插入图片描述
emplace与insert是等价的,emplace与emplace_back也是等价的,功能上都是插入数据,但是还是可以看到,insert和push_back接口插入的是一个左值或者右值,而emplace系列的接口插入的是一个参数包。

C++11以后新增了emplace系列的接口,emplace系列的接口均为可变参数的函数模板,功能上兼容push和insert系列。虽然insert和push_bac实现了插入右值的接口,但是该接口不属于万能引用,它只是一个普通的可变模板函数,不是通过参数推演,类模板示例化的,而是通过value_type实例化的,而且insert和push_back只能插入一个value_type的参数。而emplace系列的接口是通过参数来进行推演的,而且是一个万能引用,可以说功能上涵盖了insert和push_back系列,并且emplace还可以传多个参数

emplace还支持新玩法,假设容器为container,emplace还支持直接构造T对象的参数,这样有些场景就会更高效一些,可以直接在容器区间上构造T对象,如下图:
在这里插入图片描述
图片中红色跟蓝色部分的结果与push_back的结果没区别,也就是对于emplace_back接口来说,插入一个左值也是先构造+拷贝构造,插入一个右值直接走移动构造。真正的区别是橙色部分,插入一个右值,emplace直接构造了一个T对象的参数,因为emplace是可变参数的函数模板也是一个万能引用,编译器推出来的参数类型是const char*,然后直接用const char* 去构造结点里面的string,而push_back还要先用const char*构造一个string的临时对象,再走移动构造

虽说push_back是构造+移动构造,但是移动构造的效率也是足够低的,对于深拷贝的类型,就与emplace没有多大的差别,但是对于浅拷贝的类型,还是有一定的差别的,浅拷贝,若开的空间太大,也要一个字节一个字节的拷过去,所以总体上来说,还是emplace的效率更高一些

在来看如果是多参数的呢,如下面图片:
在这里插入图片描述
首先,当list存的是一个pair时,若插入的是一个有名的pair,不管是左值还是右值emplace_back和push_back并没有区别

再来看下面的图片:
在这里插入图片描述
图片中可以看到,因为emplace_back是可变参数模板的函数,所以它支持传多个参数,而push_back则不支持。又因为emplace_back是一个可变参数模板,所以编译器直接推出参数的类型为const char*和int,再用它们直接去构造一个list结点里面的pair对象。若push_back也想匿名传参加上{}即可,{“苹果”,1},因为C++11支持多参数的隐式类转换,先隐式类型转换构造一个pair的临时对象再进行移动构造

总结:push_back和insert的用法,emplace系列的也支持,但是如果是多参数的时候,push_back和insert要走隐式类型转换,而emplace系列的可以将多个参数传给参数包,参数包再进行往下传直接就构造。整体而言,用容器时,更推荐用emplace系列

1.1.3.1 简单实现list的emplace系列

实现emplace系列需要注意的就是,传过来的参数包是左值的就要保持左值属性,是右值就要保持右值属性,所以需要用到完美转发forward函数。该系列的实现与insert和push_back的实现逻辑几乎类似,还有emplace的参数包不需要对其进行包扩展,编译器会将参数包一直往下传,然后对参数进行直接匹配

代码如下:

#pragma once
namespace li
{
	template<class T>
	struct ListNode
	{
		ListNode<T>* _next;
		ListNode<T>* _prev;
		T _data;

		ListNode(const T& data = T())
			:_next(nullptr)
			, _prev(nullptr)
			, _data(data)
		{}

		ListNode(T&& data)
			:_next(nullptr)
			, _prev(nullptr)
			, _data(move(data))//右值引用的属性还是左值,需要转成右值属性
		{}
		
		//可变参数的构造
		template<class ...Args>
		ListNode(Args&& ...args)
			:_next(nullptr)
			, _prev(nullptr)
			, _data(forward<Args>(args)...) //需要保持参数的属性,因用完美转发
		{}
	};

	template<class T, class Ref, class Ptr>
	struct ListIterator
	{
		typedef ListNode<T> Node;
		typedef ListIterator<T, Ref, Ptr> Self;
		Node* _node;
		ListIterator(Node* node)
			:_node(node)
		{}
		Self& operator++()
		{
			_node = _node->_next;
			return *this;
		}

		Ref operator*()
		{
			return _node->_data;
		}

		bool operator!=(const Self& it)
		{
			return _node != it._node;
		}
	};

	template<class T>
	class list
	{
		typedef ListNode<T> Node;
	public:
		typedef ListIterator<T, T&, T*> iterator;
		typedef ListIterator<T, const T&, const T*> const_iterator;

		iterator begin()
		{
			return iterator(_head->_next);
		}

		iterator end()
		{
			return iterator(_head);
		}

		void empty_init()
		{
			_head = new Node();
			_head->_next = _head;
			_head->_prev = _head;
		}

		list()
		{
			empty_init();
		}
		
		template<class ...Args>
		void emplace_back(Args&& ...args)
		{
			emplace(end(), forward<Args>(args)...);
		}
		
		template<class ...Args>
		iterator emplace(iterator pos, Args&& ...args)
		{
			Node* cur = pos._node;
			Node* newnode = new Node(forward<Args>(args)...);
			Node* prev = cur->_prev;
			// prev newnode cur
			prev->_next = newnode;
			newnode->_prev = prev;
			newnode->_next = cur;
			cur->_prev = newnode;
			return iterator(newnode);
		}
		
	private:
		Node* _head;
	};
}

1.2 默认移动构造和移动赋值

C++11新增了两个默认成员函数,移动构造和移动赋值运算符重载。若没有自己实现移动构造函数,且没有实现析构函数、拷贝构造、拷贝赋值重载中的任意一个,那么编译器就会自动生成一个默认移动构造。默认生成的移动构造,对于内置类型成员会进行逐字节的拷贝,自定义类型成员,则需要看这个成员是否实现了移动构造,如果实现了就调用,没有实现就调用拷贝构造。默认赋值跟移动构造完全类似。若提供了移动构造和移动赋值,编译器就不会自动生成拷贝构造和拷贝赋值。
在这里插入图片描述

1.3 包装器

1.3.1 function

std::function是一个类模板,也是一个包装器。std::function实例化出来的对象可以包装存储其他的可调用对象,包括函数指针,lambda,bind表达式等,被存储的可调用对象被称为std::function的目标。
在这里插入图片描述
该图片中显示的是function的某些成员函数,其中function实现了operator(),所以function实际上也是一个仿函数

std::function的声明如下:

//(1)
template <class T> function; // undefined
//(2)
template<class Ret,class ...Args>
class function<Ret(Args...)>; //Ret是可调用函数的返回值,参数包是可调用函数的参数

可知,function是一个可变参数模板的仿函数

function的使用如下:

int f(int a, int b)
{
	return a + b;
}
struct Functor
{
	int operator()(int a, int b)
	{
		return a + b;
	}
};
class Plus
{
public:
	Plus(int n = 10)
		:_n(n)
	{ }
	static int plus(int a, int b)
	{
		return a + b;
	}
	double pplus(int a, int b)
	{
		return (a + b) * _n;
	}
private:
	int _n;
};

上述代码中,我们实现了一个函数,一个仿函数,还有一个类,类里面还有静态成员函数和非静态成员函数,它们都是不同类型的可调用函数。当我们想定义一个可调用对象的类型时,就会出现矛盾,因为有各种各样的类型。function就解决了这样的矛盾,它将不同类型的可调用函数包装成同一个类型。

#include <functional>
int main()
{
	//包装各种类型的可调用对象
	std::function<int(int, int)> f1 = f;
	std::function<int(int, int)> f2 = Functor();
	std::function<int(int, int)> f3 = [](int a, int b) {return a + b; };
	
	//调用时就可以统一类型
	std::cout << f1(1, 1) << std::endl;
	std::cout << f2(1, 1) << std::endl;
	std::cout << f3(1, 1) << std::endl;
	return 0;
}

function的头文件是functional。"<>"里面的第一个int代表包装的可调用函数的返回类型,“()”里面代表包装的可调用函数的参数。底层其实是用一个“变量”将f,Functor(),还有lambda表达式存储起来,成为function的成员变量,调用时,function的operator()就会调用对应的函数指针,f1就会调用f,f2就会调用Functor(),f3就会调用lambda表达式。这样的好处就是可以将返回类型和参数相同的可调用函数进行类型统一。若std::function没有包装的对象,调用目标为空的std::function会导致抛出std::bad_function异常。

接着来看调用成员函数的代码

#include <functional>
int main()
{
	//包装成员函数时,需要加上"&"符号才能获取地址,静态成员函数可不加
	std::function<int(int, int)> f4 = &Plus::plus;
	std::cout << f4(1, 1) << std::endl;

	//包装成员函数时,要注意还有一个this指针
	std::function<double(Plus*, double, double)> f5 = &Plus::pplus;
	Plus ps;
	std::cout << f5(&ps, 1.1, 1.1) << std::endl;
	//还可以这样写
	std::function<double(Plus&&, double, double)> f6 = &Plus::pplus;
	std::cout << f6(Plus(), 1.1, 1.1) << std::endl;
	//成员函数可以用指针调用,也可以用对象来进行调用
	return 0;
}

有包装器的原因本质上是,可调用对象的类型复杂且多,包装器就能进行对其进行统一,就是一个“套壳”

用包装器实现逆波兰表达式:

#include <iostream>
#include <vector>
#include <string>
#include <stack>
#include <map>
#include <functional>
int evalRPN(std::vector<std::string>& tokens)
{
	std::stack<int> st;
	std::map<std::string, std::function<int(int, int)>> FuncMap = {
		{"+",[](int left,int right) {return left + right; }},
		{"-",[](int left,int right) {return left - right; }},
		{"*",[](int left,int right) {return left * right; }},
		{"/",[](int left,int right) {return left / right; }}
	};

	for (auto& str : tokens)
	{
		if (FuncMap.count(str))
		{
			int right = st.top();
			st.pop();
			int left = st.top();
			st.pop();
			int ret = FuncMap[str](left, right); 
			//以前实现的逆波兰表达式要用switch case来匹配对应的计算方式,现在直接用一条命令即可
			st.push(ret);
		}
		else
		{
			st.push(std::stoi(str));
		}
	}
	return st.top();
}

1.3.2 bind(绑定)

bind的声明:

//没有返回值
template <class Fn, class... Args>
  /* unspecified */ bind (Fn&& fn, Args&&... args);

//有返回值
template <class Ret, class Fn, class... Args>
  /* unspecified */ bind (Fn&& fn, Args&&... args);

bind是一个函数模板,它是一个可调用对象的包装器,可以把它看做一个函数适配器,对接受到的 fn ,也就是可调用对象进行处理后返回一个可调用对象。bind可以用来调整参数个数和参数顺序,bind也在functional的头文件中。

调用bind的一般形式:

auto newCallable = bind(Callable,arg_list);
//newCallable本身是一个可调用对象
//arg_list是一个逗号分隔的参数列表,对应给定的Callable的参数

当调用newCallable时,newCallable会调用Callable,并传给它arg_list的参数。

注意:arg_list用 _1,_2,_3,…来表示,_1永远代表第一个参数,_2永远代表第二个参数,_3永远代表第三个参数,…,它们被封装在std::placeholders的命令空间里

一个简单的bind代码:

#include <iostream>
#include <functional>
int sub(int a, int b)
{
	return a - b;
}
int subx(int a, int b, int c)
{
	return (a - b - c) * 10;
}
using std::placeholders::_1;
using std::placeholders::_2;
using std::placeholders::_3;
int main()
{
	auto b1 = std::bind(sub, _1, _2);
	std::cout << b1(10, 5) << std::endl;
	return 0;
}

该主函数的代码,本质是对sub进行一个包装,_1代表第一个实参,_2代表第二个实参,根据这样的规则我们可以对代码进行调整,如下:
在这里插入图片描述
bind最常用的功能是调整参数的个数

在这里插入图片描述
图片中的代码是将参数固定,调用b1时就只需要传两个参数即可。

总的来说,bind是根据所给的可调用对象,对可调用进行调整之后,再生成一个新的可调用的仿函数对象。


网站公告

今日签到

点亮在社区的每一天
去签到