【C++11】深入解析C++可变参数模板

可变参数模板

在之前,如果我们想要实现一个函数可以接收不同类型的参数,我们可以通过模板来实现;那如果想要它接收不同数量的参数怎么办呢?在以前我们可以通过函数重载来实现,但是这种方法非常麻烦,要写多个函数。于是,C++11引入了可变参数模板。

一、基本语法与原理

可变参数模板 ,顾名思义,就是支持可变数量 的函数模板或类模板,即可以传不同数量的参数,可变数量的参数被称为参数包,参数包分为:模板参数包和函数参数包 。它们都能支持传入零到多个 函数参数。

经典形式:

cpp 复制代码
template <class ...Args> void Func(Args... args) {}
//左值引用
template <class ...Args> void Func(Args&... args) {}
//万能引用
template <class ...Args> void Func(Args&&... args) {}
  • 我们用省略号来指出⼀个模板参数或函数参数的表示一个包,在模板参数列表中,class...typename...指出接下来的参数表示零或多个类型列表;在函数参数列表中,类型名后面跟...指出接下来表示零或多个形参对象列表;函数参数包可以用左值引用或右值引用表示,跟前面普通模板一样,每个参数实例化时遵循引用折叠规则。
  • 上面的Args不是一个类型,是一组类型。
  • 可变参数模板的原理跟模板类似,本质还是去实例化对应类型和个数的多个函数。
  • 我们可以使用运算符 sizeof...去获取参数包中参数的个数
cpp 复制代码
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;
}

可变参数模板是在编译时就确定了参数的类型,上面的调用会结合引用折叠规则实例化出以下四个函数:

cpp 复制代码
void Print();
void Print(int&& arg1);
void Print(int&& arg1, string&& arg2);
void Print(double&& arg1, string&& arg2, double& arg3);

二、包扩展

当我们传入参数包时。我们是无法直接去使用其中的变量的,此时我们就引出包扩展以使用包中的参数

1. 参数包展开(一)------递归展开

cpp 复制代码
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...);
}

我们来看看它的递归展开图:

再来看一段代码:

cpp 复制代码
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...);
} 
// 编译时递归推导解析参数
template <class ...Args>
void Print(Args... args)
{
    ShowList(args...);
}

这是一个典型的错误,可变参数模板的参数匹配是在编译时进行的,而ShowList中的if语句是在参数确定之后才执行的,是运行时逻辑,当args是零个参数时,无法匹配到函数。

2. 参数包展开(二)------利用函数返回值

cpp 复制代码
template <class T>
const T& GetArg(const T& x)
{
    cout << x << " ";
    return x;
} 
template <class ...Args>
void Arguments(Args... args)
{}
template <class ...Args>
void Print(Args... args)
{
    // 注意GetArg必须返回获得到的对象,这样才能组成参数包给Arguments
    Arguments(GetArg(args)...);
}
int main()
{
   	Print(1, 2.2, string("hello"));
	return 0;
}

当我们调用Print时,传入的args<int, double, const char*>Print...GetArg(args)后面,因此展开后得到

cpp 复制代码
Arguments(
    GetArg(1),
    GetArg(2.5),
    GetArg("hello")
);

然后它就会依次调用GetArgs依次输出。

结果:

我们看到:结果好像反了一下。这是为什么呢?其实像上面的Arguments函数这样的参数为函数返回值的函数,编译器并没有准确规定一定要先执行谁,后执行谁,这全取决于编译器的实现。

如果我们想要正序的输出,可以用第三种初始化列表的方法。

3. 参数包展开(三)------初始化列表

cpp 复制代码
template<class... Args>
void Print(Args... args)
{
    int arr[] = { (cout << args << ' ',0)... };
    cout << endl;
}

int main()
{
    Print(1, 2.2, string("hello"));
	return 0;
}

我们借助了一个数组来依次执行输出,它的值是一个逗号表达式,表达式会先执行第一个参数cout,然后再返回第二个参数0,刚好让数组接收,...前面就是这个表达式,因此将其展开后得到:

cpp 复制代码
int arr[] =
{
    (cout << 1 << ' ', 0),
    (cout << 2.2 << ' ', 0),
    (cout << string("hello") << ' ', 0)
};

随后arr的值就是[0,0,0]

这样就能保证编译器依次执行了。

三、emplace

我们以listemplace_back为例来学习emplace系列接口。

先看定义:

可以看到,list::emplace_back是用万能引用和可变参数模板来实现的。

同样都是插入元素,emplace_backpush_back有什么区别呢?


情况一:左值

cpp 复制代码
int main()
{
	list<zzt::string> lt;
	zzt::string s1 = "1111111111";
	cout << "********************************" << endl;
	lt.push_back(s1);
	cout << "********************************" << endl;
	lt.emplace_back(s1);
	return 0;
}

输出:

可见,传左值时,两者没有任何区别。

情况二:右值

cpp 复制代码
int main()
{
	list<zzt::string> lt;
	zzt::string s1 = "1111111111";
	zzt::string s2 = "1111111111";
	cout << "********************************" << endl;
	lt.push_back(move(s1));
	cout << "********************************" << endl;
	lt.emplace_back(move(s2));

	return 0;
}

输出:

可见,传右值时,两者依旧完全相同。

情况三:直接传参

cpp 复制代码
int main()
{
	list<zzt::string> lt;
	lt.push_back("1111111111");
	cout << "********************************" << endl;
	lt.emplace_back("1111111111");
}

输出:

此时就不一样了,push_back构造+移动构造emplace_back直接构造

我们先来看看两者声明的区别:

push_back:

cpp 复制代码
void push_back (const value_type& val);
void push_back (value_type&& val);

emplace_back:

cpp 复制代码
template <class... Args>
  void emplace_back (Args&&... args);

对于push_back"1111111111"const char*类型,当我们传入"1111111111"时,由于push_back中的value_type在定义list<zzt::string> lt时就已经确定为zzt::string了,因此,编译器会先构造 一个zzt::string类型的临时对象,再移动构造val

对于emplace_back ,它的参数类型是在编译时展开确定的,因此传入const char*,它的参数类型就是const char*,然后再进行一系列传递,直至传到构造函数,直接构造

实际上,两者效率几乎相同,因为移动构造的开销非常非常小。

情况四:多参数类型场景

就以pair为例:

cpp 复制代码
int main()
{
	list<pair<zzt::string, int>> lt;
	lt.push_back({ "苹果",1 });
	cout<< "********************************" <<endl;
	lt.emplace_back("苹果", 1);
	return 0;
}

输出:

这个也是不同的,原理和上面的差不多。

对于push_back ,它只能通过花括号传参,不能直接像emplace_back一样通过多个参数来传参。

对于emplace_back,它只能通过多个参数传参,不能使用花括号,因为可变参数模板是在编译时确定类型的,传入一个花括号,它不知道一个花括号代表什么,无法推出参数类型。


总结:部分场景下,emplace可以直接构造,push和insert是构造+移动构造或构造+拷贝构造,在效率方面emplace略胜一筹,emplace综合而言更好用、更强大,因此使用时推荐用emplace替代push和insert。

相关推荐
hurrycry_小亦1 小时前
洛谷题目:P1215 [USACO1.4] 母亲的牛奶 Mother‘s Milk 题解(本题简)
算法
十五年专注C++开发1 小时前
fatal error C1189: #error: WinSock.h has already been included 的解决方案
c++·socket·boost
KJ_BioMed2 小时前
从PDB到高亲和力分子:De novo生成式计算化学Pipeline剖析
算法·生物医药·生物科研·科研干货·化合物设计
一个王同学3 小时前
从零到一 | CV转多模态大模型 | week17 | LLM 推理优化 & vLLM 详解
人工智能·深度学习·算法·机器学习·计算机视觉·vllm
jimy13 小时前
C语言模拟对象、方法:“函数指针+结构体“复用函数指针指向的函数体
c语言·开发语言
天外天-亮4 小时前
HBuilder X 使用 uview-plus 方式
开发语言·javascript·hbuilder x·uview-plus
木木子224 小时前
[特殊字符] 音乐播放器——状态驱动的多媒体控制
android·开发语言·华为·php·harmonyos
旖-旎4 小时前
《LeetCode 53 最大子数组和 || LeetCode 918 环形子数组的最大和》
c++·算法·leetcode·动态规划
变量未定义~4 小时前
单调栈+倍增思想 皇家守卫【算法赛】、单调队列 附近最小
算法