C++11 引用折叠、完美转发、可变模板参数、emplace系列接口详解

 更新时间:2026年06月13日 08:49:01   作者:Mortalbreeze  
本文详细解析了C++中引用折叠、完美转发、可变参数模板及emplace系列接口的概念与应用,涵盖引用折叠规则、完美转发原理、模板参数包与函数参数包的使用等核心概念,感兴趣的朋友一起看看吧

一、引用折叠

1.1 引用折叠的产生原因

在C++中不能定义引用的引用,但可以通过 模板 或 typedef  中的类型操作可以构成引用的引用。

示例1:不能定义引用的引用

int& &&r = i; // 编译错误

示例2:通过 typedef 中的类型操作构成引用的引用

typedef int& lref;
typedef int&& rref;
lref& r1 = n;  // r1 的类型是int& 
lref&& r2 = n; // r2 的类型是int& 
rref& r3 = n;  // r3 的类型是int& 
rref&& r4 = 1; // r4 的类型是int&&

示例3:通过 模板 中的类型操作构成引用的引用

template<class T>
void f1(T& x)
{}
int main()
{
    int n = 10;
    f1<int&&>(n);
    f1<int&>(n);
    return 0;
}

1.2 什么是引用折叠

通过模板或typedef中的类型操作可以构成引用的引用时,这时C++11将这种情况称为引用折叠。

1.3 引用折叠的规则

右值引用的右值引用折叠成右值引用,所以其他组合均折叠成左值引用。

左值引用 + 左值引用 -> 左值引用

左值引用 + 右值引用 -> 左值引用

右值引用 + 左值引用 -> 左值引用

右值引用 + 右值引用 -> 右值引用

示例1:T& x

// 由于引⽤折叠规则,f1实例化以后总是⼀个左值引⽤
template<class T>
void f1(T& x)
{}
int main()
{
    int n = 0;
    // 没有折叠 -> 实例化为 void f1(int& x)
    f1<int>(n);
    f1<int>(0); // 编译错误,0是右值,采用左值引用需要const修饰
    // 折叠 -> 实例化为 void f1(int& x)
    f1<int&>(n);    
    f1<int&>(0); // 编译错误
    // 折叠 -> 实例化为 void f1(int& x)
    f1<int&&>(n);
    f1<int&&>(0); // 编译错误
    // 没有折叠 -> 实例化为 void f1(const int& x)
    f1<const int>(n);
    f1<const int>(0);
    // 折叠 -> 实例化为 void f1(const int& x)
    f1<const int&>(n);
    f1<const int&>(0);
    // 折叠 -> 实例化为 void f1(const int& x)
    f1<const int&&>(n);
    f1<const int&&>(0);
    return 0;
}

示例2:T&& x

// 由于引用折叠限定,f2实例化后可以是左值引用,也可以是右值引用​
template<class T>
void f2(T&& x)
{}
int main()
{
    // 没有折叠 -> 实例化为void f2(int&& x)​
    f2<int>(n); // 编译错误,右值引用不能直接接受左值
    f2<int>(0);
    // 折叠 -> 实例化为 void f2(int& x)​
    f2<int&>(n);
    f2<int&>(0); // 编译错误
    // 折叠 -> 实例化为 void f2(int&& x)​
    f2<int&&>(n); // 报错​
    f2<int&&>(0);
    // 没有折叠 -> 实例化为 void f2(const int&& x)​
    f2<const int>(n); // 编译错误,右值引用不能直接接受左值
    f2<const int>(0);
    // 折叠 -> 实例化为 void f2(const int& x)​
    f2<const int&>(n);
    f2<const int&>(0); 
    // 折叠 -> 实例化为 void f2(const int&& x)​
    f2<const int&&>(n); // 编译错误
    f2<const int&&>(0);
    return 0;
}

示例3:

template<class T>
void Function(T&& t)
{
    int a = 0;
    T x = a;
    x++;
    cout << &a << endl;
    cout << &x << endl << endl;
}
int main()
{
    // 0是右值 -> T为int -> 模板实例化为 void Function(int&& t)
    // Function中 x++ 不会改变a的值
    Function(0);
    int n = 10;
    // n是左值 -> T为int& -> 模板实例化为 void Function(int& t)
    // Function中 x++ 会改变a的值
    Function(n);
    const int m = 2;
    // m是左值 -> T为const int& -> 模板实例化为 void Function(const int& t)
    // Function中 x++ 会编译错误
    Function(m);
    // std::move(m)是右值 -> T为const int -> 
    // 模板实例化为 void Function(const int&& t)
    // Function中 x++ 会编译错误
    Function(std::move(m));
    return 0;
}

总结:像f2和Function这样的函数模板,T&&x 参数看起来是右值引用参数,但由于引用折叠的规则,传递左值时就是左值引用,传递右值时就是右值引用,这种函数模板的参数被称作万能引用

二、完美转发

2.1 完美转发的产生原因

在C++中,无论是左值引用或者右值引用,它们本身的属性都是左值属性(在右值引用中,我们知道一个右值被右值引用引用后,这个右值引用变量的属性是左值)。也就是说,像上面Function中t的属性始终是左值,那么我们把t转递给另外的函数,那么匹配的都是左值引用的函数,假如我们想要保持t对象的属性,就需要借助完美转发完成。

2.2 什么是完美转发

// 重载1:接收左值引用参数(最常用)
template<typename _Tp>
constexpr _Tp&& forward(typename remove_reference<_Tp>::type& __t) noexcept
{
    return static_cast<_Tp&&>(__t);
}
// 重载2:接收右值引用参数,禁止传入左值
template<typename _Tp>
constexpr _Tp&& forward(typename remove_reference<_Tp>::type&& __t) noexcept
{
    static_assert(!is_lvalue_reference_v<_Tp>, "forward T& cannot bind rvalue");
    return static_cast<_Tp&&>(__t);
}

完美转发就是把参数原封不动地传递给其他函数,保持其左值、右值以及类型特性(如const)不变。

forward本质是一个函数模板,它主要通过引用折叠的方式来实现。

示例:

未使用完美转发

#include <iostream>
using namespace std;
void Fun(int& x) { cout << "左值引用" << endl; }
void Fun(const int& x) { cout << "const 左值引用" << endl; }
void Fun(int&& x) { cout << "右值引用" << endl; }
void Fun(const int&& x) { cout << "const 右值引用" << endl; }
template<class T>
void Function(T&& t)
{
    Fun(t);
}
int main()
{
    // 10是右值,推导出T为int,模板实例化为void Function(int&& t)​
    Function(10); // 右值​
    int a = 0;
    // a是左值,推导出T为int&,引用折叠,模板实例化为void Function(int& t)​
    Function(a); // 左值​
    // move(a)是右值,推导出T为int,模板实例化为void Function(int&& t)​
    Function(move(a)); // 右值​
    const int b = 8;
    // a是左值,推导出T为const int&,引用折叠,模板实例化为void Function(const int& t)
    Function(b); // const 左值​
    // move(b)右值,推导出T为const int,模板实例化为void Function(const int&& t)
    Function(move(b)); // const 右值​
    return 0;
}

代码运行结果:

左值引用
左值引用
左值引用
const 左值引用
const 左值引用

使用完美转发

#include <iostream>
using namespace std;
void Fun(int& x) { cout << "左值引用" << endl; }
void Fun(const int& x) { cout << "const 左值引用" << endl; }
void Fun(int&& x) { cout << "右值引用" << endl; }
void Fun(const int&& x) { cout << "const 右值引用" << endl; }
template<class T>
void Function(T&& t)
{
    //Fun(t);
    Fun(forward<T>(t));
}
int main()
{
    // 10是右值,推导出T为int,模板实例化为void Function(int&& t)​
    Function(10); // 右值​
    int a = 0;
    // a是左值,推导出T为int&,引用折叠,模板实例化为void Function(int& t)​
    Function(a); // 左值​
    // move(a)是右值,推导出T为int,模板实例化为void Function(int&& t)​
    Function(move(a)); // 右值​
    const int b = 8;
    // a是左值,推导出T为const int&,引用折叠,模板实例化为void Function(const int& t)
    Function(b); // const 左值​
    // move(b)右值,推导出T为const int,模板实例化为void Function(const int&& t)
    Function(move(b)); // const 右值​
    return 0;
}

代码运行结果:

右值引用
左值引用
右值引用
const 左值引用
const 右值引用

三、可变参数模板

3.1 什么是可变参数模板

可变参数模板就是参数数量可变的函数模板和类模板,可变数目的参数被称为参数包,参数包有两种存在形式:模板参数包,表示零或多个模板参数;函数参数包,表示零或多个函数参数。

3.2 基本语法

// 传值传参
template <class... Args> 
void Func(Args... args)
{
        // ...
}
// 左值引用传参 ---> 遵守引用折叠的规则
template <class... Args> 
void Func(Args&... args)
{
        // ...
}
// 右值引用传参 ---> 遵守引用折叠的规则
template <class... Args> 
void Func(Args&&... args)
{
        // ...
}
templagte <class ...Args> 这种写法也是支持的

说明:我们用省略号来指出一个模板参数或函数参数表示一个包,这就指出template <class ...Args> 中的Args可以是任何合法的变量名,它会被实例化为类型名称;在函数参数列表中,类型名称后面跟...指出接下来表示零或多个参数;函数参数包可以是传值作为参数,也可以用左值引用或右值引用,但每个参数实例化时需要遵守引用折叠规则

3.4 如何理解参数包

template<class ...Args>
void Func(Args... args) {}
这里实际上有两个参数包
1. 模板参数包
对于template<class ...Args>, Args是模板参数包
Args 可以代表零个或多个类型
例如:
Func(10, 3.14, "hello");
编译器推导:
Args = <int, double, const char*>
此时Args对应了一组类型 int double const char*,为模板参数包
2. 函数参数包
对于Args... args,args是函数参数包
args 可以代表零个或多个参数
例如:
Func(10, 3.14, "hello");
调用函数时,args = <10, 3.14, "hello">
此时args对于了一组实参,为函数参数包

对于上面的示例,很多人误认为Args... 整体叫做参数包,其实不完全正确。

对于 template<class... Args> 含义是 class... 告诉编译器后面的 Args 是一个模板参数包。

因此 Args 是模板参数包的名字。

对于 Args... args 含义是 Args... 告诉编译器后面的 args 是一个函数参数包。

因此 args 是函数参数包的名字。

... 表示:这个位置展开为多个参数。

3.4 sizeof...运算符

sizeof...运算符用来计算参数包中参数的个数。

#include <iostream>
using namespace std;
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;
}

运行结果:

0

1

2

3

3.5 包扩展

对于一个参数包,我们该如何使用里面的参数呢?包扩展就是解决这个问题的。

包扩展就是把参数包里的每个元素依次取出来,按照某种模式展开。

template<class... Args>
void Func(Args... args)
{
    Print(args...);
}
调用:
Func(1,2,3);
推导:
Args = <int,int,int>
args = <1,2,3>
Print(args...);  这里的args... 就是包扩展
Print(1,2,3);

最经典的例子 ---- 不常用

#include <iostream>
using namespace std;
template<class... Args>
void Func(Args... args)
{
    // Args 推导出的类型必须相同或者可以强转才能放到数组中(如double,char)
    int arr[] = { args... };
	for(int i = 0; i < sizeof...(args); ++i)
	{
		cout << arr[i] << " ";
	}
	cout << endl;
}
int main()
{
	Func(1,2,3);
	return 0;
}

递归展开参数包 ---- C++11

#include <iostream>
using namespace std;
void ShowList()
{
	// 编译器时递归的终止条件,参数包是0个时,直接匹配这个函数​
	cout << endl;
}
template <class T, class... Args>
void ShowList(T x, Args... args)
{
	cout << x << " ";
	ShowList(args...);
}
// 编译时递归推导解析参数​
template <class... Args>
void Print(Args... args)
{
    // args是N个参数的参数包​
	// 调用ShowList,参数包的第一个传给x,剩下N-1传给第二个参数包​
	ShowList(args...);
}
int main()
{
	Print(1, string("xxxxx"), 2.2);
	return 0;
}

函数调用展开 ---- C++11

#include <iostream>
using namespace std;
template <class T>
const T &GetArg(const T &x)
{
	cout << x << " ";
	return x;
}
template <class... Args>
void Arguments(Args... args)
{
	cout << endl;
}
template <class... Args>
void Print(Args... args)
{
	// 注意GetArg必须返回或者到的对象,这样才能组成参数包给Arguments​
	Arguments(GetArg(args)...);
}
int main()
{
	Print(1, 'a', string("1111"), 1.1);
	return 0;
}

折叠表达式 ---- C++17

#include <iostream>
using namespace std;
template<class... Args>
void ShowList(Args... args)
{
    ((cout << args << " "), ...);
    cout << endl;
}
int main()
{
	ShowList(1, 'a', string("1111"), 1.1);
	return 0;
}

3.6 可变参数模板的原理

总结:

模板:一个函数模板实例化出多个不同类型参数的函数

可变参数模板:一个可变参数函数模板实例化出多个不同参数个数的函数模板,进而实例化出多个不同类型参数的函数。

四、 emplace系列接口

4.1 emplace的意义

在C++11以后,STL容器新增了emplace系列的接口,emplace系列的接口均为可变参数模板,功能上兼容push和insert系列,但emplace还支持新的特性 ---- 可以直接在容器空间上构造对象,在某些场景下,效率会高于push和insert系列。推荐使用emplace系列替代insert和push系列。

4.2 emplace的接口

template <class... Args> void emplace_back (Args&&... args);
template <class... Args> iterator emplace (const_iterator position,Args&&... args);

4.3 emplace系列与push和insert系列的区别

vector<string> v;
string s1 = "111111";
// 传左值,效果一样,均是拷贝构造
v.push_back(s1);
v.emplace_back(s1);
// 传右值,效果一样,均是移动构造
v.push_back(string("2222"));
v.emplace_back(string("2222"));
// 传需要隐式类型转换的对象
v.push_back("3333");
v.emplace_back("4444");
// vector 中的push_back函数
// void push_back (const value_type& val);
// void push_back (value_type&& val);
// 对于push_back,v 已经进行实例化,value_type已经被推导为 string
// 所以对于传过来 const char* 对象会构造出一个临时对象string("3333")
// 这个临时对象再去移动构造 v 中的对象
// template <class... Args>
// void emplace_back (Args&&... args);
// 对于emplace_back, v 的实例化不会影响可变参数模板,所以传过来 const char*
// 会推导为 const char* && 来直接构造 v 中的对象
// 多参数对象
// 直接传左值和右值,push_back 和 emplace_back 效率一样
// 对于隐式类型转换的参数
vector<pair<string, string>> arr;
arr.push_back({"apple", "苹果"});
arr.emplace_back("sort", "排序");
// emplace_back 为构造,push_back 为构造 + 移动构造
// 值得注意一点:在emplace_back中会通过完美转发将参数的属性原封不动地传下去

总结:

push_back 和 emplace_back 的区别

push_back 接收的是已经构造好的对象。

emplace_back 接收的是构造对象所需的参数。

因此:

push_back 是“先构造对象,再放入容器”;
emplace_back 是“直接在容器内部构造对象”。

什么时候效率一样?

当对象已经存在时,两者基本没有区别。

例如已经有一个字符串对象,无论使用 push_back 还是 emplace_back,本质上都是拷贝或移动这个对象。

什么时候 emplace_back 更有优势?

当传入的不是对象,而是构造对象所需的参数时。

此时 push_back 往往需要先生成一个临时对象,再放入容器;而 emplace_back 可以直接在容器内部构造目标对象,减少一次中间过程。

为什么 emplace_back 能做到?

因为它使用了:

  • 可变参数模板
  • 完美转发
  • 原地构造

它会把收到的参数原封不动地转交给元素类型的构造函数,并直接在容器分配好的内存上构造对象。

多参数对象是 emplace 的最佳场景

对于需要多个参数才能构造的对象,例如键值对、映射节点、自定义类等:

emplace 可以直接把这些参数传给构造函数,而 push 系列通常需要先构造一个完整对象再插入。

因此优势最明显。

面试高频总结

  • push_back 与 emplace_back 的本质区别是什么?
  • push_back 插入对象,emplace_back 构造对象。
  • emplace_back 一定比 push_back 快吗?
  • 不一定。对象已经存在时,两者通常没有明显区别。
  • 什么时候应该优先使用 emplace_back?
  • 当需要根据参数现场构造对象时。
  • emplace_back 底层依赖什么技术?
  • 可变参数模板、完美转发和原地构造。
  • 为什么 STL 后来增加了 emplace 系列接口?
  • 为了减少不必要的临时对象,提高构造复杂对象时的效率。

到此这篇关于C++11 引用折叠、完美转发、可变模板参数、emplace系列接口详解的文章就介绍到这了,更多相关C++11 引用折叠内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • C语言实现简单图书管理系统

    C语言实现简单图书管理系统

    这篇文章主要为大家详细介绍了C语言实现图书管理系统,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2018-01-01
  • C/C++自定义类型结构体全解析

    C/C++自定义类型结构体全解析

    c语言有内置类型(char short int long flaot double long double),也有自定义类型—结构体(struct) 枚举(enum) 联合体(union) 本文介绍结构体,感兴趣的朋友跟随小编一起看看吧
    2025-05-05
  • Qt中QUdpSocket类的简单使用详解

    Qt中QUdpSocket类的简单使用详解

    文章介绍了QUdpSocket类的基本使用方法,包括发送端和接收端的代码实现,QUdpSocket是Qt中用于UDP通信的类,它支持无连接通信、广播和数据包的发送与接收,文章还提到了数据编码和对比TCP的简化点,感兴趣的朋友跟随小编一起看看吧
    2026-01-01
  • C语言实现简单的飞机大战游戏

    C语言实现简单的飞机大战游戏

    这篇文章主要为大家详细介绍了C语言实现简单的飞机大战游戏,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2022-05-05
  • C++控制台绘图头文件实例代码

    C++控制台绘图头文件实例代码

    控制台(console)是电脑的最基本交互接口,通常包括键盘(keyboard)和屏幕(screen),下面这篇文章主要给大家介绍了关于C++控制台绘图头文件的相关资料,文中通过示例代码介绍的非常详细,需要的朋友可以参考下
    2023-01-01
  • C语言基础双指针移除元素解法

    C语言基础双指针移除元素解法

    这篇文章介绍了C语言基础双指针移除元素的解法,文中通过示例代码介绍的非常详细。对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2021-12-12
  • C语言中文件读取中文乱码问题解析与解决方案

    C语言中文件读取中文乱码问题解析与解决方案

    在C语言编程中,文件操作是常见任务之一,然而,当读取包含中文的文本文件时,开发者常常会遇到 "烫烫烫"乱码 或 中文显示异常 的问题,本文将深入分析这些问题的根源,并提供完整的解决方案,需要的朋友可以参考下
    2025-05-05
  • C++基础入门教程(三):数组、字符串、结构体、共用体

    C++基础入门教程(三):数组、字符串、结构体、共用体

    这篇文章主要介绍了C++基础入门教程(三):数组、字符串、结构体、共用体,需要的朋友可以参考下
    2014-11-11
  • 详解C++的JSON静态链接库JsonCpp的使用方法

    详解C++的JSON静态链接库JsonCpp的使用方法

    这篇文章主要介绍了C++的JSON静态链接库JsonCpp的使用方法,演示了使用JsonCpp生成和解析JSON的方法,以及C++通过JSON方式的socket通信示例,需要的朋友可以参考下
    2016-03-03
  • C语言实现随机抽奖程序

    C语言实现随机抽奖程序

    这篇文章主要为大家详细介绍了C语言实现随机抽奖程序,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2021-09-09

最新评论