C++ 变长参数模板

一、什么是变长参数模板

C++11 中新增加了一项内容,叫做变长参数模板,所谓变参数模板,顾名思义就是参数个数和类型都可能发生变化的模板,要实现这一点,那就必须要使用模板形参包。

模板形参包是可以接受 0 个或者 n 个模板实参的模板形参,至少有一个模板形参包的模板就可以称作变长参数模板。因此,搞懂了模板形参包就明白变参数模板了,因为变参数模板就是基于模板形参包来实现的,接下来我们就来看看到底啥是模板形参包。

二、模板参数包

模板形参包主要出现在函数模板和类模板中,目前来讲,模板形参包主要有三种,即:非类型模板形参包、类型模板形参包、模板模板形参包。

2.1 非类型模板形参包

非类型模板形参包语法是这样的:

1
template<类型 ... args>

模板使用typename或者class关键字表示它们后面跟着的名称是类型名称,而这里的形参包里面类型其实表示一个固定的类型,所以这里其实不如叫做固定类型模板形参包。

对于上述非类型模板形参包而言,类型选择一个固定的类型,args 其实是一个可修改的参数名,如下:

1
template<int ... data> xxxxxx;

注意,这个固定的类型是有限制的,标准 C++ 规定,只能为整型、指针和引用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 这里加一个空模板函数是为了编译可以通过,否则编译期间调用printAmt<int>(int&)就会找不到可匹配的函数
// 模板参数第一个类型实际上是用不到的,但是这里必须要加上,否则就是调用printAmt<>(int&),模板实参为空,但是模板形参列表是不能为空的
template <class type>
void printAmt(int &iSumAge)
{
return;
}

template <class type, int age0, int... age>
void printAmt(int &iSumAge)
{
iSumAge += age0;
// 这里sizeof ... (age)是计算形参包里的形参个数,返回类型是std::size_t,后续同理
if ((sizeof...(age)) > 0)
{
// 这里的age...其实就是语法中的一种包展开,这个后续会具体说明
printAmt<type, age...>(iSumAge);
}
}

int main()
{
int sumAge = 0;
printAmt<int, 1, 2, 3, 4, 5, 7, 6, 8>(sumAge);
cout << "the sum of age is " << sumAge << endl;
return 0;
}

由上代码可以看出:

  • 非类型模板形参包类型是固定的,但参数名跟普通函数参数一样,是可以修改的;
  • 传递给非类型模板形参包的实参不是类型,而是实际的值。

2.2 类型模板形参包

类型模板形参包语法如下:

1
template <typename ... Args> // template<class ... Args>

typename 关键字和 class 关键字都可以用于在模板中声明一个未知类型,只是在以前 template 的基础上加了一个省略号,改成了可变形参包而已,该可变形参包可以接受无限个不同的实参类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
void xprintf()
{
cout << endl;
}

template <typename T, typename... Targs>
void xprintf(T value, Targs... Fargs)
{
cout << value << ' ';
if ((sizeof...(Fargs)) > 0)
{
// 这里调用的时候没有显式指定模板,是因为函数模板可以根据函数参数自动推导
xprintf(Fargs...);
}
else
{
xprintf();
}
}

int main()
{
xprintf("小明个人信息:", "小明", "男", 35, "程序员", 169.5);
return 0;
}

2.3 模板模板形参包

这个就有点绕了,模板模板形参包,有点不好理解,还是先看一下语法看看:

1
template < 形参列表 > class ... Args(可选)

其实说白了,就是说这个形参包本身它也是一个模板,在看模板模板形参包之前,我们先介绍一下模板模板形参,因为形参包说白了,就是在形参的基础上增加了省略号实现的。

我们先看一下标准库中对模板模板形参的使用,找到头文件bits/alloc_traits.h,在模板类allocator_traits的声明中有这样一个结构体,如下:

1
2
3
4
5
template<template<typename> class _Func, typename _Tp>
struct _Ptr<_Func, _Tp, __void_t<_Func<_Alloc>>>
{
using type = _Func<_Alloc>;
};

这里的意思就是说_Func这个模板形参本身是一个带模板的类型,使用的时候是需要声明模板实参的。

假设有这样一种场景,我们需要定义一个vector变量,但不能确定vector的元素类型,此时该怎么办呢?

看如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <typeinfo>
#include <cxxabi.h>
#include <iostream>
#include <vector>

//将gcc编译出来的类型翻译为真实的类型
const char* GetRealType(const char* p_szSingleType)
{
const char* szRealType = abi::__cxa_demangle(p_szSingleType, nullptr, nullptr, nullptr);
return szRealType;
}

//这里的func是一个模板模板形参
template<template<typename, typename> class func, typename tp, typename alloc = std::allocator<tp> >
struct temp_traits
{
using type = func<tp, alloc>;
type tt;//根据模板类型定义一个成员变量
};

int main()
{
temp_traits<std::vector, int> _traits;
//获取结构体字段tt的类型
const std::type_info &info = typeid(_traits.tt);
std::cout << GetRealType(info.name()) << std::endl;
return 0;
}

/* output
std::vector<int, std::allocator<int> >
*/

这里类型temp_tratis里面根据模板模板形参和其他模板形参来实现了我们的使用场景。

理解了模板模板形参,再来看看模板模板形参包的使用,这个与类型模板形参包没什么两样,只不过类型换成了一个带模板的类型而已,看下面这段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
#include <typeinfo>
#include <cxxabi.h>
#include <iostream>
#include <vector>
#include <deque>
#include <list>

//将gcc编译出来的类型翻译为真实的类型
const char* GetRealType(const char* p_szSingleType)
{
const char* szRealType = abi::__cxa_demangle(p_szSingleType, nullptr, nullptr, nullptr);
return szRealType;
}

//泛化变参模板
template<typename tp, typename alloc, template<typename, typename> class ... types >
struct temp_traits
{
temp_traits(tp _tp)
{
std::cout << "泛化模板执行" << std::endl;
}
};

//偏特化变参模板
template< typename tp, typename alloc, template<typename, typename> class type, template<typename, typename> class ... types >
struct temp_traits<tp, alloc,type, types...>:public temp_traits<tp, alloc, types...>
{
using end_type = type<tp, alloc>;
end_type m_object;
temp_traits(tp _tp)
:temp_traits<tp, alloc, types...>(_tp)
{
const std::type_info &info = typeid(m_object);
std::cout << "偏特化版本执行, 此时类型:" << GetRealType(info.name()) << std::endl;
m_object.push_back(_tp);
}
void print()
{
auto it = m_object.begin();
for(;it != m_object.end(); ++it)
{
std::cout << "类型为:" << GetRealType(typeid(end_type).name()) << ", 数据为:" << *it << std::endl;
}
}
};

int main()
{
temp_traits<int, std::allocator<int>, std::vector, std::deque, std::list> _traits(100);
_traits.print();
return 0;
}

/* output
泛化模板执行
偏特化版本执行, 此时类型:std::__cxx11::list<int, std::allocator<int> >
偏特化版本执行, 此时类型:std::deque<int, std::allocator<int> >
偏特化版本执行, 此时类型:std::vector<int, std::allocator<int> >
类型为:std::vector<int, std::allocator<int> >, 数据为:100
*/

根据4次构造函数的调用,我们可以得出结论:形参包包含多少个形参,它就会在此基础上有几层继承,所以现在是3个形参,3层继承,顶层基类是泛化模板,然后进行了三层派生,这个递归继承的过程是编译器根据代码自行展开的。

再看看对于成员函数print的调用,我的原意是想针对每一种容器类型,都打印出结果,但现在只打印了一种,我们可以想想,对于继承,非虚函数但函数类型相同的情况下,派生类的成员函数会覆盖基类的成员函数,所以这里结果是正常的。

那么怎么实现我们要的效果呢,答案是使用析构函数,层层析构,所以将成员函数print函数修改为如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
~temp_traits()
{
auto it = m_object.begin();
for(;it != m_object.end(); ++it)
{
std::cout << "类型为:" << GetRealType(typeid(end_type).name()) << ", 数据为:" << *it << std::endl;
}
}

/* output
泛化模板执行
偏特化版本执行, 此时类型:std::__cxx11::list<int, std::allocator<int> >
偏特化版本执行, 此时类型:std::deque<int, std::allocator<int> >
偏特化版本执行, 此时类型:std::vector<int, std::allocator<int> >
类型为:std::vector<int, std::allocator<int> >, 数据为:100
类型为:std::deque<int, std::allocator<int> >, 数据为:100
类型为:std::__cxx11::list<int, std::allocator<int> >, 数据为:100
*/

到这里,我们对模板模板形参包应该就有了比较深的了解了。

注意,不论是哪种形参包,形参包都需要放在模板的最后面,否则编译就会有问题。

三、函数形参包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
//将gcc编译出来的类型翻译为真实的类型
const char* GetRealType(const char* p_szSingleType)
{
const char* szRealType = abi::__cxa_demangle(p_szSingleType, nullptr, nullptr, nullptr);
return szRealType;
}

void xprintf()
{
std::cout << "调用空函数" << std::endl;
}

template<typename tp, typename alloc, template<typename, typename> class T, template<typename, typename> class ... Targs >
void xprintf(T<tp, alloc> value, Targs<tp, alloc>... Fargs)
{
std::cout << "容器类型:" << GetRealType(typeid(value).name()) << std::endl;
std::cout << "容器数据:" << std::endl;
auto it = value.begin();
for(; it != value.end(); ++it)
{
std::cout << *it << ',';
}
std::cout << std::endl;
if ( (sizeof ...(Fargs)) > 0 )
{
//这里调用的时候没有显式指定模板,是因为函数模板可以根据函数参数自动推导
xprintf(Fargs...);
}
else
{
xprintf();
}
}

int main()
{
std::vector<int> vt;
std::deque<int> dq;
std::list<int> ls;
for(int i =0 ; i < 10 ; ++i)
{
vt.push_back(i);
dq.push_back(i);
ls.push_back(i);
}

xprintf(vt, dq, ls);
return 0;
}

这个就是一个典型的使用模板模板形参包类型作为函数形参的案例,说白了,我们要理解函数形参包的本质,它其实还是一个函数形参,既然是函数形参,就脱离不了类型加参数名的语法,形参包无非就是在类型后面加个省略号,而模板模板形参包作为函数形参类型的时候一定要记得加模板参数,比如代码里面T这样才是一个完整的类型,光是一个T,它的类型就是不完整的。

四、模板形参包的展开

1
模式 ...

在模式后面加省略号,就是包展开了,而所谓的模式一般都是形参包名称或者形参包的引用,包展开以后就变成零个或者多个逗号分隔的实参。

比如上面的age ...Fargs...都属于包展开,但是要知道,这种形式我们是没有办法直接使用的,那么具体该怎么使用呢,有两种办法:

  • 一是使用递归的办法把形参包里面的参数一个一个的拿出来进行处理,最后以一个默认的函数或者特化模板类来结束递归;
  • 二是直接把整个形参包展开以后传递给某个适合的函数或者类型。

递归方法适用场景:多个不同类型和数量的参数有比较相似的动作的时候,比较适合使用递归的办法。

关于递归办法的使用,前面几节有多个案例了,这里不再展开多说。

关于整个形参包传递的使用方法,看下面代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
#include <iostream>
#include <string>
using namespace std;

class programmer
{
string name;
string sex;
int age;
string vocation;//职业
double height;
public:
programmer(string name, string sex, int age, string vocation, double height)
:name(name), sex(sex), age(age), vocation(vocation), height(height)
{
cout << "call programmer" << endl;
}

void print()
{
cout << "name:" << name << endl;
cout << "sex:" << sex << endl;
cout << "age:" << age << endl;
cout << "vocation:" << vocation << endl;
cout << "height:" << height << endl;

}
};

template<typename T>
class xprintf
{
T * t;
public:
xprintf()
:t(nullptr)
{}

template<typename ... Args>
void alloc(Args ... args)
{
t = new T(args...);
}

void print()
{
t->print();
}

void afree()
{
if ( t != nullptr )
{
delete t;
t = nullptr;
}
}
};

int main()
{
xprintf<programmer> xp;
xp.alloc("小明", "男", 35, "程序员", 169.5);
xp.print();
xp.afree();
return 0;
}

这里类型xprintf是一个通用接口,类模板中类型T是一个未知类型,我们不知道它的构造需要哪些类型、多少个参数,所以这里就可以在它的成员函数中使用变参数模板,来直接把整个形参包传递给构造函数,具体需要哪些实参就根据模板类型T的实参类型来决定。


C++ 变长参数模板
http://example.com/2026/05/27/C++-变长参数模板/
作者
Yu xin
发布于
2026年5月27日
许可协议