【C++】C++11可变参数模板和emplace系列接口

目录

[1. 可变参数模版](#1. 可变参数模版)

[1. 1 核心概念](#1. 1 核心概念)

[1.2 常用参数传递方式](#1.2 常用参数传递方式)

[1.3 编译期核心流程](#1.3 编译期核心流程)

[1.4 核心优势](#1.4 核心优势)

[2. 包扩展](#2. 包扩展)

[2.1 常见的几种包扩展方式](#2.1 常见的几种包扩展方式)

[方式 1:递归包扩展(C++11 经典方式)](#方式 1:递归包扩展(C++11 经典方式))

[方式 2:初始化列表 + 逗号表达式(非递归方式)](#方式 2:初始化列表 + 逗号表达式(非递归方式))

方式3:函数调用实现包扩展(非递归方式)

[2.2 几种包扩展方式对比](#2.2 几种包扩展方式对比)

[3. 与 emplace 系列接口的关联](#3. 与 emplace 系列接口的关联)

[二. emplace系列接口](#二. emplace系列接口)

[1. 传统接口 vs emplace 接口](#1. 传统接口 vs emplace 接口)

[2. 双向链表实现中的 emplace 接口](#2. 双向链表实现中的 emplace 接口)

[3. 核心设计思想总结](#3. 核心设计思想总结)

[4. 最佳实践建议](#4. 最佳实践建议)


1. 可变参数模版

1.1 核心概念

可变参数模板的核心是参数包(Parameter Pack),分为两种:

  • 模板参数包 :template <typename... Args>,Args 是类型参数包(可包含任意数量的类型)
  • 函数参数包 :void func(Args... args),args 是值参数包(可包含任意数量的对应类型的值)

其中 ... 是参数包展开符,是可变参数模板的核心,所有操作最终都围绕「展开参数包」展开。

注意:Argsargs 是自定义的名字(你可以写成 T.../t...),但惯例用 Args 表示类型包,args 表示值包。

1.2 常用参数传递方式

复制代码
// 值传递
template <class... Args>
void Func(Args... args) {}

// 左值引用传递
template <class... Args>
void Func(Args&... args) {}

// 万能引用传递(推荐,支持完美转发)
template <class... Args>
void Func(Args&&... args) {}

1.3 编译期核心流程

代码示例:

复制代码
template<class ...Args>
void Print(Args&&...args)
{
	cout << sizeof...(args) << endl;
}

int main()
{
	double x = 2.2;

	Print();
	Print(1);
	Print(1, string("11111111"));
	Print(1, string("11111111"), x);
    return 0;
}

//编译器会为每一组不同的实参类型/数量,生成独立的函数实例
//编译器实例化出以下四个函数:
void Print<>() { cout << 0 << endl; }
void Print<int>(int&& __arg1) { cout << 1 << endl; }
void Print<int, std::string>(int&& __arg1, std::string&& __arg2) { cout << 2 << endl; }
void Print<int, std::string, double&>(int&& __arg1, std::string&& __arg2, double& __arg3) { cout << 3 << endl; }

sizeof...(args) 是参数个数 :它和 sizeof 运算符不同,专门用于统计参数包中元素的数量,是编译期常量

以 Print(1, string("11111111"), x); 为例:

步骤1:推导模版参数包: 编译器逐个分析实参,推导 Args = <int, std::string, double&>。

  • 实参 1:1int 类型的右值 → 推导为**int**
  • 实参 2:string("11111111")std::string 类型的右值 → 推导为**std::string**
  • 实参 3:xdouble 类型的左值 → 推导为**double&** (左值会被推导为左值引用)最终,模板参数包 Args 被推导为:Args = <int, std::string, double&>

步骤 2:应用「引用折叠」,确定函数参数类型

函数参数是 Args&&... args,编译器会对每个 Args 类型执行引用折叠,确定最终函数参数类型:

推导后的 Args 类型 Args&& 展开 引用折叠结果 最终参数类型
int int&& 无折叠(纯右值) int&&
std::string std::string&& 无折叠(纯右值) std::string&&
double& double& && 折叠为 double& double&

步骤 3:生成「具体的函数实例」

编译器会根据推导结果,生成一个全新的、非模板的函数(这个函数会被编译进二进制文件):

复制代码
// 编译器自动生成的实例化函数(你写的代码里看不到,但二进制里存在)
void Print<int, std::string, double&>(int&& __arg1, std::string&& __arg2, double& __arg3) 
{
    cout << 3 << endl; // sizeof...(args)是编译期常量,直接替换为3
}

本质:模板是代码生成器,编译器为每一组不同的实参类型 / 数量,生成独立的函数实例。

核心:编译器根据实参推导模板参数包→ 应用引用折叠 → 生成具体的非模板函数(sizeof... 直接替换为常量)。

1.4 核心优势

如果没有可变参数模版,我们需要为每一种参数数量都写一个独立模版:

  • 1 个参数:template<class T1> void Print(T1&& arg1)
  • 2 个参数:template<class T1, class T2> void Print(T1&& arg1, T2&& arg2)
  • 3 个参数:template<class T1, class T2, class T3> ...
  • ...... 无限重复,代码冗余。

可变参数模板的意义在于

  • 消除代码冗余:无需为每种参数数量写独立模板。
  • 灵活性升级:让 C++ 模板从「处理固定个数的任意类型」升级为「处理任意个数的任意类型」。

2. 包扩展

2.1 常见的几种包扩展方式

方式 1:递归包扩展(C++11 经典方式)

这是 C++11 中最经典的包扩展方式,核心逻辑是「每次拆出第一个参数,剩下的参数包继续递归」,必须搭配递归终止函数(参数包为空时调用)。

代码示例(打印任意参数):

复制代码
// 递归终止函数:参数包为空时调用(必须!)
void print() 
{
	cout << "包扩展完成" << endl;
}

// 可变参数模板函数:递归拆包
template <typename T, typename... Args>
void print(T first, Args... rest) //可根据需要写成万能引用 T&& first,Args&& ...rest
{
	// 处理第一个参数(核心:只处理单个参数,不碰包)
	cout << first << endl;
	// 扩展剩余参数包:rest... 把剩余参数传入下一次递归
	print(rest...);
}

int main() 
{
	print(10, 3.14, string("hello"), 'A'); // 测试任意参数
	return 0;
}

底层逻辑:

复制代码
//错误写法:
void print()
{
	cout << "包扩展完成" << endl;
}

//编译时递归推导解析参数
template <typename T, typename... Args>
void print(T&& first, Args&&... rest)
{	
	if (sizeof...(args) == 0)
		return;

	cout << first << endl;
	print(rest...); 
}

核心原因:普通if是运行时判断,但参数包的递归展开是编译期行为 ,编译器会先检查print(rest...)的合法性(rest 为空时无法匹配带参模板函数),根本到不了运行时的 if 判断,因此必须单独写无参终止函数。

总结:

递归展开的核心是逐层剥除第一个参数:每次处理一个参数,剩余参数包继续递归,直到为空。

终止函数是递归的出口:必须存在无参版本,否则递归无法停止。

所有展开逻辑在编译期完成:运行只是依次调用编译器生成的多个函数实例,无额外开销。

方式 2:初始化列表 + 逗号表达式(非递归方式)

利用 std::initializer_list 遍历参数包的特性,结合逗号表达式逐个执行操作,避免递归。

复制代码
#include <iostream>
#include <string>
#include <initializer_list> // 需包含头文件
using namespace std;

template <typename... Args>
void print(Args... args) 
{
	// 核心:初始化列表遍历参数包,逗号表达式执行操作
	(void)initializer_list<int>{
		// 对每个args执行:打印 → 返回0(给初始化列表用)
		(cout << args << " " , 0)...
    };
}

int main() 
{
	print(1, 2.5, "test"); // 输出3个参数
	return 0;
}

std::initializer_list是 C++11 引入的轻量级容器 std::initializer_list是 C++11 为支持{}初始化设计的只读临时值列表,核心是接收一组同类型值,基础用途是让函数 / 类支持{}初始化(如 STL 容器)

原理拆解:

initializer_list的巧妙用法------利用其"遍历同类型值"的特性,强制编译器展开参数包

  • initializer_list<int>{...}:创建一个 int 类型的初始化列表,编译器会执行每个(cout << args << " " , 0)表达式,填充列表。
  • (cout << args << " " , 0):逗号表达式,先执行打印参数的逻辑,再返回0(给初始化列表提供合法的int值)。
  • ...展开符:放在表达式末尾,告诉编译器对参数包args里的每一个元素,都执行一次这个逗号表达式。
  • (void):避免编译器警告 "初始化列表变量未被使用"(因为我们只利用初始化列表的遍历特性,不需要使用这个列表对象),不影响功能。

总结:

  • **非递归包扩展,**用初始化列表 + 逗号表达式替代递归,无需写终止函数;
  • 所有展开逻辑在编译期完成,运行时仅执行打印,无额外开销;
  • 是 C++11 中实现任意参数遍历的经典写法,比递归更简洁。
方式3:函数调用实现包扩展(非递归方式)

利用函数调用时的参数包扩展,把对每个参数的处理逻辑嵌入到另一个函数调用中。

复制代码
template <class T>
int GetArg(const T& x)
{
	cout << x << " "; // 处理单个参数:打印 
	return 0; // 必须返回值!否则无法作为Arguments的实参
}

// Arguments 可以是空函数它的唯一作用是 "接住" 扩展后的参数列表,让编译器完成对每个args的GetArg调用,本身不需要实现任何逻辑。
template <class ...Args>
void Arguments(Args... args)
{}

template <class ...Args>
void Print(Args... args)
{
    //GetArg(args)...; //error! C++语法不允许在独立的表达式语句后使用包展开,...展开操作必须发生在"可接受参数列表的地方"
	Arguments(GetArg(args)...); //对参数包args中的每一个元素,都先调用GetArg(单个参数),再把所有GetArg的返回值作为参数包,传入Arguments函数。
}

int main()
{
	double x = 2.2;
	Print(1, string("11111111"), x);

	return 0;
}

执行流程

调用Print(1, string("11111111"), x);时,编译器会把GetArg(args)...展开为:

复制代码
Arguments(GetArg(1),GetArg("11111111"),GetArg(x));

执行顺序:

  • 依次调用GetArg(1) → 打印1,返回0
  • 依次调用GetArg("11111111") → 打印11111111,返回0
  • 依次调用GetArg(x) → 打印2.2,返回0
  • 最后调用Arguments(0, 0, 0),但这个函数是空的,什么都不做。

与方式2对比:

  • 本质相同:都是在编译期把参数包拆成单个元素,逐个执行处理逻辑。
  • 区别只是载体不同:

初始化列表+逗号表达式:用initializer_list<int>{......}承载拓展;

函数调用:用Arguments(...)函数调用承载扩展。

总结:

  • GetArg 处理单个参数;
  • Arguments(...) 作为载体,把 GetArg(args)... 扩展成完整的参数列表;
  • 最终效果:遍历参数包,对每个参数执行 GetArg 逻辑,实现和递归展开一样的功能,但代码更简洁、无递归终止函数。

2.2 几种包扩展方式对比

方式 优点 缺点 适用场景
递归包扩展 直观易懂,符合递归思维 必须写终止函数,代码稍繁琐 适合理解包扩展原理
初始化列表 + 逗号表达式 代码简洁,无终止函数 依赖 initializer_list,逗号表达式可读性稍弱 适合快速实现简单遍历操作
函数调用式包扩展 灵活可扩展,无终止函数 需要额外载体函数,理解成本稍高 适合封装复杂参数处理逻辑

3. 与 emplace 系列接口的关联

可变参数模板是 emplace 系列接口的核心技术基础:

  • emplace_back(Args&&... args) 利用可变参数模板接收构造参数。
  • 通过 std::forward<Args>(args)... 完美转发参数,在容器内存中就地构造元素,避免临时对象的拷贝 / 移动。

二. emplace系列接口

emplace系列是C++11引入的容器接口(如emplace_back / emplace_front / emplace),核心优势是就地构造元素 ,避免临时对象的创建和拷贝/移动,大幅提升性能(尤其对拷贝成本高的类型,如std::string/自定义大对象)

1. 传统接口 vs emplace 接口

特性 传统接口(push_back/push_front) emplace 系列接口
构造方式 先创建临时对象 → 拷贝 / 移动到容器 → 销毁临时对象 直接在容器的内存空间里构造对象
开销 至少 1 次拷贝 / 移动 + 1 次析构 0 次拷贝 / 移动,仅 1 次构造
设计初衷 传入已构造好的对象 传入构造参数,最大化减少拷贝/移动

2. 双向链表实现中的 emplace 接口

以双向链表为例,完善实现emplace_back / emplace_front / emplace 接口。

复制代码
#include <iostream> 
using namespace std;

namespace pig
{
	template<class T>
	struct list_node
	{
		T _data;
		list_node<T>* _next;
		list_node<T>* _prev;

		// C++11 可变参数模板构造
		template <class... Args>
		list_node(Args&&... args)
			:_data(std::forward<Args>(args)...) 
			, _next(nullptr)
			, _prev(nullptr)
		{}	
	};

	//迭代器省略......

	template<class T>
	class list
	{
	private:
		typedef list_node<T> Node;

		Node* _head;
		size_t _size;

	public:
		typedef list_iterator<T, T&, T*> iterator;
		typedef list_iterator<T, const T&, const T*>  const_iterator;

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

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

		const_iterator begin()const
		{
			return const_iterator(_head->_next);  
		}

		const_iterator end()const
		{
			return const_iterator(_head);  
		}

		//空初始化
		void empty_init()
		{
			_head = new Node();
			_head->_next = _head;
			_head->_prev = _head;
			_size = 0;
		}

		//默认构造
		list()
		{
			empty_init();
		}

		list(initializer_list<T> il)
		{
			empty_init();
			for (auto& e : il)
			{
				push_back(e);
			}
		}

		// 拷贝构造
		list(const list<T>& lt)
		{
			empty_init();
			for (auto& e : lt)
			{
				push_back(e);
			}
		}

		// C++11 移动构造
		list(list<T>&& lt) noexcept  
			:_head(lt._head)
			, _size(lt._size)
		{
			// 置空原链表,避免析构时重复释放
			lt._head = nullptr;
			lt._size = 0;
		}

		// 拷贝赋值
		list<T>& operator=(list<T> lt)
		{
			swap(lt);
			return *this;
		}

		// C++11 移动赋值
		list<T>& operator=(list<T>&& lt) noexcept
		{
			swap(lt);
			return *this;
		}

		//析构函数
		~list()
		{
			clear();
			delete _head;
			_head = nullptr;

			_size = 0;
			cout << "链表已销毁" << endl;
		}

		void clear()
		{
			auto it = begin();
			while (it != end())
			{
				it = erase(it);
			}
		}

		void swap(list<T>& lt)
		{
			std::swap(_head, lt._head);
			std::swap(_size, lt._size);
		}

		iterator erase(iterator pos)
		{
			assert(pos != end());

			Node* cur = pos._node;
			Node* prev = cur->_prev;
			Node* next = cur->_next;

			//prev  cur  next
			prev->_next = next;
			next->_prev = prev;

			delete cur;
			--_size;
			return next;
		}

		void push_back(const T& x) { insert(end(), x); }	
		void push_back(T&& x){insert(end(), std::move(x));}	// C++11 右值版本push_back

		void push_front(const T& x){insert(begin(), x);}
		void push_front(T&& x){insert(begin(), std::move(x));}// C++11 右值版本push_front

		iterator insert(iterator pos, const T& val)
		{
			Node* cur = pos._node;
			Node* prev = cur->_prev;
			Node* newnode = new Node(val);//先拷贝构造Node的_data

			newnode->_next = cur;
			newnode->_prev = prev;
			prev->_next = newnode;
			cur->_prev = newnode;

			++_size;
			return iterator(newnode);  
		}

		// C++11 右值版本insert
		iterator insert(iterator pos, T&& val)
		{
			Node* cur = pos._node;
			Node* prev = cur->_prev;
			Node* newnode = new Node(std::move(val));  // 先移动构造Node的_data

			newnode->_next = cur;
			newnode->_prev = prev;
			prev->_next = newnode;
			cur->_prev = newnode;

			++_size;
			return iterator(newnode);
		}

		// C++11 emplace核心:就地构造节点(无需先构造T对象)
		template <typename... Args>
		iterator emplace(iterator pos, Args&&... args)
		{
			Node* cur = pos._node;
			Node* prev = cur->_prev;
			// 直接用构造参数构造Node的_data,无拷贝/移动
			Node* newnode = new Node(std::forward<Args>(args)...);

			newnode->_next = cur;
			newnode->_prev = prev;
			prev->_next = newnode;
			cur->_prev = newnode;

			++_size;
			return iterator(newnode);
		}

		// C++11 emplace_back:尾插
		template <typename... Args>
		void emplace_back(Args&&... args)
		{
			emplace(end(), std::forward<Args>(args)...);
		}

		// C++11 emplace_front:头插
		template <typename... Args>
		void emplace_front(Args&&... args)
		{
			emplace(begin(), std::forward<Args>(args)...);
		}
		
	};
}

自定义 string 类(模拟高拷贝成本类型)

复制代码
namespace cat
{
	class string
	{
	public:

		//默认构造
		string(const char* str = "")
			:_str(nullptr)
			, _size(0)
			, _capacity(0)
		{
			cout << "构造" << endl;

			if (str == nullptr)
			{
				_str = new char[1];
				*_str = '\0';
			}
			else
			{
				_size = strlen(str);
				_capacity = _size;
				_str = new char[_capacity + 1];
				strcpy(_str, str);
			}
		}

		//拷贝构造
		string(const string& s)
			:_str(nullptr)
			, _size(0)
			, _capacity(0)
		{
			cout << "拷贝构造" << endl;
			_size = s._size;
			_capacity = s._capacity;
			_str = new char[_capacity + 1];
			strcpy(_str, s._str);
		}

		void swap(string& s)
		{
			::swap(_str, s._str);
			::swap(_size, s._size);
			::swap(_capacity, s._capacity);
		}

		//移动构造
		string(string&& s) 
			:_str(nullptr)
			,_size(0)
			,_capacity(0)
		{
			cout << "移动构造" << endl;

			swap(s);
		}

		//拷贝赋值
		string& operator=(const string& s)
		{
			if (this != &s)
			{
				char* new_str = new char[s._capacity + 1];
				strcpy(new_str, s._str);

				delete[] _str;

				_str = new_str;
				_size = s._size;
				_capacity = s._capacity;
			}
			return *this;
		}

		//移动赋值
		string& operator=(string&& s)
		{
			cout << "移动赋值" << endl;
			swap(s);

			return *this;
		}

		~string()
		{
			//cout << "析构" << endl;
			delete[] _str;
			_str = nullptr;
		}
	private:
		char* _str = nullptr;
		size_t _size = 0;
		size_t _capacity = 0;
	};
}

测试代码:

复制代码
#include"List.h"

int main()
{

	pig::list<cat::string> lt;

	cat::string s1("111111111"); //构造
	cat::string s2("222222222"); //构造

	cout << "******************" << endl;

	//传左值:都走拷贝构造
	lt.push_back(s1); 
	lt.emplace_back(s1); 
	cout << "******************" << endl;

	//传右值:都走移动构造
	lt.push_back(move(s1));
	lt.emplace_back(move(s2)); 
	cout << "******************" << endl;

	//直接传构造参数:
	//单个参数:
	lt.push_back("11111111");    //走隐式类型转换 (构造+移动构造)
	lt.emplace_back("11111111"); //构造
	cout << "******************" << endl;

	//多个参数:
	pig::list<pair<cat::string, int>> lt2;

	//lt2.push_back("苹果", 5);//不支持这样写!
	lt2.push_back({ "苹果", 5 });//走隐式类型转换(构造 + 移动构造)
	lt2.emplace_back("苹果", 5); //构造 直接用参数包构造pair
	cout << "******************" << endl;

	return 0;
}

3. 核心设计思想总结

  1. 完美转发std::forward<Args>(args)... 保证参数以原始值类别(左值 / 右值)传递给T的构造函数,避免额外拷贝。
  2. 就地构造 :直接在容器分配的节点内存中构造T对象,无需先创建临时对象再拷贝 / 移动。
  3. 接口复用emplace_back/emplace_front 复用emplace,代码简洁且易维护。
  4. 性能优势
    • 单参数构造emplace避免临时对象创建,比push_back少 1 次构造 + 1 次移动。
    • 多参数构造emplace是唯一支持直接传参的方式,push_back必须先构造临时对象。

4. 最佳实践建议

  • 优先使用emplace_back/emplace_front,尤其是:
    • 元素类型拷贝 / 移动成本高(如自定义大对象、std::string
    • 需要直接传递构造参数(尤其是多参数场景)
  • 当需要传递已构造好的左值 / 右值对象 时,push_backemplace_back性能差异不大,可根据可读性选择。
相关推荐
ab1515172 小时前
3.21二刷基础125、122、130,完成进阶65
开发语言·c++·算法
j_xxx404_2 小时前
力扣--分治(快速排序)算法题I:颜色分类,排序数组
数据结构·c++·算法·leetcode·排序算法
阿Y加油吧2 小时前
力扣打卡day08——轮转数组、除自身外乘积
数据结构·算法·leetcode
代码探秘者2 小时前
【算法篇】2.滑动窗口
java·数据结构·后端·python·算法·spring
像素猎人2 小时前
数组中的二分查找函数:lower_bound【第一个 >= 目标值的元素的值或者下标】 和 upper_bound【第一个 > 目标值的元素的值或者下标】
数据结构·算法
crediks2 小时前
MTGR(美团生成式推荐框架)总结文档
人工智能·深度学习·算法
im_AMBER2 小时前
Leetcode 143 搜索插入位置 | 搜索二维矩阵
数据结构·算法·leetcode
小年糕是糕手2 小时前
【35天从0开始备战蓝桥杯 -- Day5】
数据结构·数据库·c++·算法·蓝桥杯
bbbb3652 小时前
算法优化的多层缓存映射与访问调度模型的技术7
算法