C++11可变参数模板、emplace系列接口、包装器

发布于:2025-09-06 ⋅ 阅读:(15) ⋅ 点赞:(0)

目录

可变参数模板

基本语法及原理

参数包的展开方式

递归展开参数包

STL容器中的emplace相关接口函数

包装器

function包装器

function包装器介绍

function包装器的意义

bind包装器

bind包装器介绍

bind包装器绑定固定参数


可变参数模板

基本语法及原理

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

我们用省略号来指出⼀个模板参数或函数参数的表示⼀个包,在模板参数列表中,class...或 typename...指出接下来的参数表示零或多个类型列表;在函数参数列表中,类型名后面跟...指出 接下来表示零或多个形参对象列表;函数参数包可以用左值引用或右值引用表示,跟前面普通模板 ⼀样,每个参数实例化时遵循引用折叠规则。

可变参数模板的原理跟模板类似,本质还是去实例化对应类型和个数的多个函数。

下面我们可以使用sizeof...运算符去计算参数包中参数的个数。

template <class ...Args>
void Print(Args&&... args)
{
	cout << sizeof...(args) << endl;//获取参数包中参数的个数
}

int main()
{
	double x = 2.2;

	Print(); // 包里有0个参数
	Print(1); // 包里有1个参数
	Print(1, string("xxxxx")); // 包里有2个参数
	Print(1.1, string("xxxxx"), x); // 包里有3个参数

	return 0;
}

// 编译本质这⾥会结合引⽤折叠规则实例化出以下四个函数

void Print();
void Print(int&& arg1);
void Print(int&& arg1, string&& arg2);
void Print(double&& arg1, string&& arg2, double& arg3);

但是我们无法直接获取参数包中的每个参数,只能通过展开参数包的方式来获取,这是使用可变参数模板的一个主要特点,也是最大的难点。

特别注意,语法并不支持使用args[i]的方式来获取参数包中的参数。比如:

template <class ...Args>
void Print(Args... args)
{
	// 可变参数模板编译时解析
	// 下面是运行获取和解析,所以不支持这样用
	cout << sizeof...(args) << endl;
	for (size_t i = 0; i < sizeof...(args); i++)
	{
		cout << args[i] << " ";
	}
}

int main()
{
	double x = 2.2;
	Print(1, string("xxxxx"), x); // 包里有3个参数

	return 0;
}

参数包的展开方式

递归展开参数包

递归展开参数包的方式如下:

  • 给函数模板增加一个模板参数,这样就可以从接收到的参数包中分离出一个参数出来。
  • 在函数模板中递归调用该函数模板,调用时传入剩下的参数包。
  • 如此递归下去,每次分离出参数包中的一个参数,直到参数包中的所有参数都被取出来。

比如我们要打印调用函数时传入的各个参数,那么函数模板可以这样编写:
 

template <class T, class ...Args>
void ShowList(T x, Args... args)
{
	cout << x << " ";
	// args是N个参数的参数包
	// 调用ShowList,参数包的第一个传给x,剩下N-1传给第二个参数包
	ShowList(args...);
}

那么在递归中,我们怎么最后怎么终止递归呢?其实并不难,我们可以在刚才的基础上,再编写一个无参的递归终止函数,该函数的函数名与展开函数的函数名相同。如下:

//编译递归推导的包扩展
void ShowList()
{
	// 编译器时递归的终止条件,参数包是0个时,直接匹配这个函数
	cout << endl;
}

这样一来,当递归调用ShowList函数模板时,如果传入的参数包中参数的个数为0,那么就会匹配到这个无参的递归终止函数,这样就结束了递归。

但如果外部调用ShowList函数时就没有传入参数,那么就会直接匹配到无参的递归终止函数。
而我们本意是想让外部调用ShowList函数时匹配的都是函数模板,并不是让外部调用时直接匹配到这个递归终止函数。
鉴于此,我们可以提供一个供外部调用的函数Print(),它可以接受任意数量、任意类型的参数

 //编译递归推导的包扩展
void ShowList()
{
	// 编译器时递归的终止条件,参数包是0个时,直接匹配这个函数
	cout << endl;
}
template <class T, class ...Args>
void ShowList(T x, Args... args)
{
	cout << x << " ";
	// args是N个参数的参数包
	// 调用ShowList,参数包的第一个传给x,剩下N-1传给第二个参数包
	ShowList(args...);
}

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

为空就终止递归,为什么不能在递归内部判断为空来终止递归?

比如下面代码这样

template <class T, class ...Args>
void ShowList(T x, Args... args)
{
	if (sizeof...(args) == 0)
		return;
	cout << x << " ";
	// args是N个参数的参数包
	// 调用ShowList,参数包的第一个传给x,剩下N-1传给第二个参数包
	ShowList(args...);
}

这种方式是不可行的,原因如下:

编译时递归 vs 运行时递归

C++ 的模板递归是编译时展开​的,编译器必须在编译期间生成所有可能的函数实例。即使 if (sizeof...(args) == 0)在运行时判断为 true,编译器仍然需要实例化所有可能的递归调用路径,包括 ShowList(args...)的所有可能版本。

args为空时,ShowList(args...)会尝试调用 ShowList(),但此时没有匹配的函数重载​(除非你显式定义了一个无参数的 ShowList()),因此 编译失败。

STL容器中的emplace相关接口函数

emplace版本的插入接口

C++11标准给STL中的容器增加emplace版本的插入接口,比如list容器的push_front、push_back和insert函数,都增加了对应的emplace_front、emplace_back和emplace函数。如下:

这些emplace版本的插入接口支持模板的可变参数,比如list容器的emplace_back函数的声明如下:

注:&&是万能模板而非右值引用

emplace系列接口的使用方式

emplace系列接口的使用方式与容器原有的插入接口的使用方式类似,但又有一些不同之处。

以list容器的emplace_back和push_back为例:

  • 调用push_back函数插入元素时,可以传入左值对象或者右值对象,也可以使用列表进行初始化。
  • 调用emplace_back函数插入元素时,也可以传入左值对象或者右值对象,但不可以使用列表进行初始化。
  • 除此之外,emplace系列接口最大的特点就是,插入元素时可以传入用于构造元素的参数包。
     

代码如下

int main()
{
	lzg::list<lzg::string> lt;
	// 传左值,跟push_back一样,走拷贝构造
	lzg::string s1("111111111111");
	lt.emplace_back(s1);
	cout << "*********************************" << endl;

	// 右值,跟push_back一样,走移动构造
	lt.emplace_back(move(s1));
	cout << "*********************************" << endl;

	// 直接把构造string参数包往下传,直接用string参数包构造string
	// 这里达到的效果是push_back做不到的
    //push_back只能走隐式类型转换变成构造+移动构造
	lt.emplace_back("111111111111");
	cout << "*********************************" << endl << endl << endl;

	lzg::list<pair<lzg::string, int>> lt1;
	// 跟push_back一样
	// 构造pair + 拷贝/移动构造pair到list的节点中data上
	pair<lzg::string, int> kv("苹果", 1);
	lt1.emplace_back(kv);
	cout << "*********************************" << endl;

	// 跟push_back一样
	lt1.emplace_back(move(kv));
	cout << "*********************************" << endl;

	// 直接把构造pair参数包往下传,直接用pair参数包构造pair
    //const char*+int
	// 这里达到的效果是push_back做不到的
    //push_back对于多参数的隐式类型转换要用花括号
	//lt1.push_back({"苹果", 1 });
	lt1.emplace_back("苹果", 1);
	cout << "*********************************" << endl;

	return 0;
}

emplace接口的意义:

  • emplace系列接口最大的特点就是支持传入参数包,用这些参数包直接构造出对象,这样就能减少一次拷贝,这就是为什么有人说emplace系列接口更高效的原因。
  • 但emplace系列接口并不是在所有场景下都比原有的插入接口高效,如果传入的是左值对象或右值对象,那么emplace系列接口的效率其实和原有的插入接口的效率是差不多的(编译器优化)。
  • emplace系列接口真正高效的情况是传入参数包的时候,直接通过参数包构造出对象,避免了中途的一次拷贝。

包装器

function包装器

function包装器介绍

function包装器

function是一种函数包装器,也叫做适配器。它可以对可调用对象进行包装,C++中的function本质就是一个类模板。

function类模板的原型如下:

template <class T> function;     // undefined
template <class Ret, class... Args>
class function<Ret(Args...)>;
  • std::function是一个类模板,也是一个包装器。std::function的实例对象可以包装存储其他的可调用对象,包括函数指针、仿函数、lambda、bind表达式等,存储的可调用对象被称为 std::function的目标。若 std::function不含目标,则称它为空。调用空的 std::function会导致抛出 std::bad_function_call异常。
  • 以上是 function的原型,它被定义在 <functional>头文件中。std::function - cppreference.comfunction的官方文档链接。
  • 函数指针、仿函数、lambda 等可调用对象的类型各不相同,std::function的优势就是统一类型,对它们都可以进行包装。这样在很多地方就方便声明可调用对象的类型。
class Plus
{
public:
	Plus(int n = 10)
		:_n(n)
	{}
	static int plusi(int a, int b)
	{
		return a + b;
	}
	double plusd(double a, double b)
	{
		return (a + b) * _n;
	}
private:
	int _n;
};

int main()
{
	int(*pf)(int, int) = f;

	// 包装各种可调用对象
	function<int(int, int)> f1 = f;
	function<int(int, int)> f2 = Functor();
	function<int(int, int)> f3 = [](int a, int b) {return a + b; };

	cout << f1(1, 1) << endl;
	cout << f2(1, 1) << endl;
	cout << f3(1, 1) << endl;

	// 包装静态成员函数
	// 成员函数要指定类域并且前面加&才能获取地址
	function<int(int, int)> f4 = &Plus::plusi;
	cout << f4(1, 1) << endl;

	function<double(Plus*, double, double)> f5 = &Plus::plusd;
	Plus ps;
	cout << f5(&ps,1.1, 1.1) << endl;

	function<double(Plus&&, double, double)> f6 = &Plus::plusd;
	cout << f6(Plus(), 1.1, 1.1) << endl;

	return 0;
}
function包装器的意义
  • 将可调用对象的类型进行统一,便于我们对其进行统一化管理。
  • 包装后明确了可调用对象的返回值和形参类型,更加方便使用者使用。

bind包装器

bind包装器介绍

bind包装器

bind也是一种函数包装器,也叫做适配器。它可以接受一个可调用对象,生成一个新的可调用对象来“适应”原对象的参数列表,C++中的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);
  • std::bind是一个函数模板,也是一个可调用对象的包装器,可以把它看作一个函数适配器,对接收的 fn可调用对象进行处理后返回一个新的可调用对象。
  • std::bind可以用来调整参数个数和参数顺序。std::bind同样定义在 <functional>头文件中。

调用 std::bind的一般形式:

auto newCallable = std::bind(callable, arg_list);
  • 其中,newCallable本身是一个可调用对象,arg_list是一个逗号分隔的参数列表,对应给定的 callable的参数。当我们调用 newCallable时,newCallable会调用 callable,并传递给它 arg_list中的参数。
  • std::bindarg_list中的参数可能包含形如 _n的名字,其中 n是一个整数。这些参数是占位符,表示 newCallable的参数,它们占据了传递给 newCallable的参数的位置。数值 n表示生成的可调用对象中参数的位置:_1表示 newCallable的第一个参数,_2表示第二个参数,以此类推。
  • _1_2_3……这些占位符定义在 std::placeholders命名空间中,使用时需要指定该命名空间。
bind包装器绑定固定参数

无意义的绑定

下面这种绑定就是无意义的绑定:

int Plus(int a, int b)
{
	return a + b;
}
int main()
{
	//无意义的绑定
	function<int(int, int)> func = bind(Plus, placeholders::_1, placeholders::_2);
	cout << func(1, 2) << endl; //3
	return 0;
}

绑定时第一个参数传入函数指针这个可调用对象,但后续传入的要绑定的参数列表依次是placeholders::_1和placeholders::_2,表示后续调用新生成的可调用对象时,传入的第一个参数传给placeholders::_1,传入的第二个参数传给placeholders::_2。此时绑定后生成的新的可调用对象的传参方式,和原来没有绑定的可调用对象是一样的,所以说这是一个无意义的绑定。

绑定固定参数

如果想把Plus函数的第二个参数固定绑定为10,可以在绑定时将参数列表的placeholders::_2设置为10。比如:

int Plus(int a, int b)
{
	return a + b;
}
int main()
{
	//绑定固定参数
	function<int(int)> func = bind(Plus, placeholders::_1, 10);
	cout << func(2) << endl; //12
	return 0;
}

此时调用绑定后新生成的可调用对象时就只需要传入一个参数,它会将该值与10相加后的结果进行返回。


网站公告

今日签到

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