首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >【C++篇】C++11:可变参数模板

【C++篇】C++11:可变参数模板

作者头像
我想吃余
发布2025-08-11 08:29:16
发布2025-08-11 08:29:16
2330
举报
文章被收录于专栏:C语言学习C语言学习

前言

其实可变参数是我们的老熟人了,我们平时使用的printf、scanf,它们的函数参数就是可变参数。

  • 以我们平时的使用很好理解,函数的可变参数就是可以有任意个的参数可以被函数接收,具体的形参参数由我们提供的实参来推演。
  • 可变参数的底层非常简单,它是用一个数组将实参的类型存储起来,然后输出到函数形参中。

C++11看中了可变参数的优势,随之可变参数模板应运而生……

一、基本语法及原理

C++11的新特性可变参数模板能够让我们创建可以接受可变参数的函数模板和类模板。

  • 相比C++98/03,类模版和函数模版中只能含固定数量的模版参数,可变模版参数无疑是一个巨大的改进。
  • 然而由于可变模版参数比较抽象,使用起来需要一定的技巧,所以这块还是比较晦涩的。
  • 因此,本文我们学习一些基础的可变参数模板特性,这对我们日常使用足够了。
代码语言:javascript
复制
// Args是一个模板参数包,args是一个函数形参参数包
// 声明一个参数包Args...args,这个参数包中可以包含0到任意个模板参数。
template <class ...Args>
void ShowList(Args... args)
{}

注意:

  • 上面的参数args前面有省略号,所以它就是一个可变模版参数,我们把带省略号的参数称为“参数包”。
  • 参数包里面包含了0到
N(N≥0)

个模版参数。

  • 参数包名字的是可以任意指定的,并不是说必须叫做Args和args。

现在,我们可以传入任意数量和任意类型的参数到ShowList中了

代码语言:javascript
复制
int main()
{
	ShowList();
	ShowList(1);
	ShowList(1, 1.1);
	ShowList(1, 1.1, 'a');
	ShowList(1, 1.1, 'a', string("woxiangchiyu"));
	
	return 0;
}

sizeof…运算符计算参数包中参数的个数

使用方式: sizeof...(参数包)

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

二、展开参数包的方式

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

递归函数方式展开参数包

具体逻辑:

  1. 给函数模板添加一个模板参数,用于从接收到的函数包中分离出一个参数出来
  2. 递归调用自身函数,调用时只传参数包。
  3. 不断递归,每次都会分离出一个参数出来,直到参数包中没有参数
代码语言:javascript
复制
template <class T, class ...Args>
void _ShowList(T val, Args... args)
{
	cout << val << " ";//打印分离出的第一个参数
	_ShowList(args...);//递归调用自身函数,调用时只传参数包
}
  1. 递归结束函数:重载一个无参的该函数,当参数包没有参数时,就会调用该函数,递归结束。
代码语言:javascript
复制
// 结束条件的函数
void _ShowList()
{
	cout << endl;
}
  1. 方便调用,可以再套一层函数模板,将被添加的模板参数封装起来,调用时传参数包即可
代码语言:javascript
复制
template <class ...Args>
void CppPrint(Args... args)
{
	_ShowList(args...);
}

递归函数代码如下:

代码语言:javascript
复制
// 结束条件的函数
void _ShowList()
{
	cout << endl;
}

//递归函数展开参数包
template <class T, class ...Args>
void _ShowList(T val, Args... args)
{
	cout << val << " ";
	_ShowList(args...);
}

//套一层,封装被添加的模板参数,调用时传参数包即可
template <class ...Args>
void CppPrint(Args... args)
{
	_ShowList(args...);
}

int main()
{
	CppPrint();
	CppPrint(1);
	CppPrint(1, 2);
	CppPrint(1, 2, 2.2);
	CppPrint(1, 2, 2.2, string("xxxx"));

	return 0;
}

可以看到,我们已经模拟实现了一个printf。

逗号表达式展开参数包

我们先来看代码:

代码语言:javascript
复制
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', std::string("sort"));
	 
	 return 0;
}

是不是一头雾水?没关系,我会详细讲解。

我们看这里初始化数组的列表:{ (PrintArg(args), 0)... }

  • ()里边是逗号表达式,可变参数的需要写在逗号表达式外面,表示需要将逗号表达式展开,参数包有几个参数,就会生成几个逗号表达式,每个参数都会先后分配到每个逗号表达式中。
    • 比如说有参数包中有3个参数,{ (PrintArg(args), 0)... }就会展开成{(PrintArg(arg1), 0), (PrintArg(arg2), 0), (PrintArg(arg3), 0)}
  • 逗号表达式会从左到右依次计算各个表达式,并且将最后一个表达式的值作为返回值进行返回。
    • 因为整型数组要求每个元素的类型都为整型,所以这里将逗号表达式的最后一个表达式设置为一个整型0,以确保每个逗号表达式返回的是一个整型值。
    • 每个逗号表达式都会去调用PrintArg(args),因而达到了参数包展开的目的

三、STL容器中的empalce相关接口函数

以list为例:

可以看到,每一个插入接口,在C++11都给配置了一个emplace的插入版本,它相对于普通接口又有何提升呢?

首先我们看到的emplace系列的接口,支持模板的可变参数,并且万能引用。

注意:这里不是右值引用,是万能引用。

emplace系列接口的使用方式

emplace系列接口的使用方式也普通插入接口使用方式大体相同

  • 调用push_back函数插入元素时,由于重载了右值引用版本,所以可以传入左值对象或者右值对象,也可以使用列表进行初始化。
  • 调用emplace_back函数插入元素时,由于可变模板参数的类型都是万能引用,所以也可以传入左值对象或者右值对象,但是不可以使用列表进行初始化。
  • 最大的不同是,emplace系列接口插入元素时可以传入用于构造元素的参数包。

示例:

代码语言:javascript
复制
int main()
{
	 std::list< std::pair<int, char> > mylist;
	 // emplace_back支持可变参数,可以用构建pair对象的参数后自己去创建一个匿名pair对象,然后用于构造list节点对象
	 mylist.emplace_back(10, 'a');//支持传入参数包构造
	 mylist.emplace_back(20, 'b');
	 //也可以像push_back一样直接用对象构造
	 mylist.emplace_back(make_pair(30, 'c'));
	 //不能像push_back一样直接用列表初始化,会报错
	 mylist.emplace_back({ 60, 'f' });//错误写法
	 
	 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系列接口的意义

我们知道,emplace系列接口调用时,既可以接收左值对象,也可以接收右值对象,还可以接收参数包。

下面我们试一下带有拷贝构造和移动构造的string,以list为例,在emplace_back和push_back接口分别传入左值对象和右值对象,来看看它们在性能上的区别:

分析:

  • 调用emplace系列接口时传入的是左值对象
    1. new对象:调用ListNode的构造函数构造出一个左值对象
    2. ListNode的构造函数构造一个自定义类的左值又会去调用该类的拷贝构造
  • 调用emplace系列接口时传入的是右值对象
    1. new对象:调用ListNode的构造函数构造出一个右值对象
    2. ListNode的构造函数构造一个自定义类的右值又会去调用该类的移动构造

说明:我们在移动构造和构造函数及拷贝构造内部各添加了一个打印提示语,方便我们观察调用情况。

总结:

  • 如果传入左值对象:调用构造函数+拷贝构造函数。
  • 如果传入右值对象:调用构造函数+移动构造函数。
  • 如果传入参数包:只需要调用构造函数即可

我们会发现其实差别也不大,emplace_back是直接构造了,push_back是先构造,再移动构造。

我们知道,移动构造的代价是非常小的,相对于深拷贝甚至可以忽略不计。

因此,在需要深拷贝的类中,emplace系列接口价值不大。

但在浅拷贝的类中,更能体现出emplace系列接口的价值,我们来看看在Date类(浅拷贝的类)中,emplace系列接口对性能的提升效果如何? 说明:这里简要实现了一个日期类,在其构造函数和拷贝构造中各打印一条提示语,便于观察。 我们接着测试list的emplace_back和push_back接口:

  • 对于push_back,只能调用日期类对象
  • 对于emplace_back,不仅可以调用日期类对象,还可以调参数包
代码语言:javascript
复制
#include <iostream>
using namespace std;

// 日期类
class Date
{
public:
    // 构造函数(带默认参数)
    // 参数:
    //   year - 年份(默认1)
    //   month - 月份(默认1)
    //   day - 日(默认1)
    Date(int year = 1, int month = 1, int day = 1)
        :_year(year)      // 初始化_year
        , _month(month)   // 初始化_month
        , _day(day)       // 初始化_day
    {
        cout << "Date构造" << endl;  // 构造时打印信息
    }

    // 拷贝构造函数
    // 参数:
    //   d - 要拷贝的Date对象
    Date(const Date& d)
        :_year(d._year)  
        , _month(d._month) 
        , _day(d._day)  
    {
        cout << "Date拷贝构造" << endl;  // 拷贝构造时打印信息
    }

private:
    int _year;   // 年
    int _month;  // 月
    int _day;    // 日
};

// 可变参数模板函数:创建Date对象
// 模板参数:
//   Args - 可变参数类型包
// 函数参数:
//   args - 可变参数包(可以匹配0到多个参数)
template <class ...Args>
Date* Create(Args... args)
{
    // 使用new在堆上创建Date对象,完美转发args参数
    Date* ret = new Date(args...);
    
    return ret;  // 返回创建的Date指针
}

int main()
{
   std::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
    1. 传日期类对象,只需要调用一次拷贝构造
    2. 传参数包,只需要调用一次构造函数,且可以灵活传参,优势明显

可以看到,emplace系列接口较普通插入接口,可以少调用一次构造/拷贝构造,当对象较为庞大时,性能的提升是不小的。

最后总结一下emplace系列接口的优势:

  • emplace系列接口最大的特点就是支持传入参数包,用这些参数包直接构造出对象,这样就能减少一次拷贝,这就是为什么有人说emplace系列接口更高效的原因,但在深拷贝类中效果微乎其微(移动构造代价太小),在浅拷贝类中效果可能更明显(对象越大,效果越大)。
  • emplace系列接口得益于可变参数模板,如果每个成员设定好缺省值,则可以灵活传参。
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2025-08-07,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 一、基本语法及原理
  • 二、展开参数包的方式
    • 递归函数方式展开参数包
    • 逗号表达式展开参数包
  • 三、STL容器中的empalce相关接口函数
    • emplace系列接口的使用方式
    • emplace系列接口的意义
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档