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

目录

一、引用折叠

[1.1 引用折叠的产生原因](#1.1 引用折叠的产生原因)

[1.2 什么是引用折叠](#1.2 什么是引用折叠)

[1.3 引用折叠的规则](#1.3 引用折叠的规则)

二、完美转发

[2.1 完美转发的产生原因](#2.1 完美转发的产生原因)

[2.2 什么是完美转发](#2.2 什么是完美转发)

三、可变参数模板

[3.1 什么是可变参数模板](#3.1 什么是可变参数模板)

[3.2 基本语法](#3.2 基本语法)

[3.4 如何理解参数包](#3.4 如何理解参数包)

[3.4 sizeof...运算符](#3.4 sizeof...运算符)

[3.5 包扩展](#3.5 包扩展)

[最经典的例子 ---- 不常用](#最经典的例子 ---- 不常用)

[递归展开参数包 ---- C++11](#递归展开参数包 ---- C++11)

[函数调用展开 ---- C++11](#函数调用展开 ---- C++11)

[折叠表达式 ---- C++17](#折叠表达式 ---- C++17)

[3.6 可变参数模板的原理](#3.6 可变参数模板的原理)

[四、 emplace系列接口](#四、 emplace系列接口)

[4.1 emplace的意义](#4.1 emplace的意义)

[4.2 emplace的接口](#4.2 emplace的接口)

[4.3 emplace系列与push和insert系列的区别](#4.3 emplace系列与push和insert系列的区别)

总结:

[push_back 和 emplace_back 的区别](#push_back 和 emplace_back 的区别)

什么时候效率一样?

[什么时候 emplace_back 更有优势?](#什么时候 emplace_back 更有优势?)

[为什么 emplace_back 能做到?](#为什么 emplace_back 能做到?)

[多参数对象是 emplace 的最佳场景](#多参数对象是 emplace 的最佳场景)

面试高频总结


一、引用折叠

1.1 引用折叠的产生原因

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

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

cpp 复制代码
int& &&r = i; // 编译错误

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

cpp 复制代码
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:通过 模板 中的类型操作构成引用的引用

cpp 复制代码
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

cpp 复制代码
// 由于引⽤折叠规则,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

cpp 复制代码
// 由于引用折叠限定,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:

cpp 复制代码
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 什么是完美转发

cpp 复制代码
// 重载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本质是一个函数模板,它主要通过引用折叠的方式来实现。

示例:

未使用完美转发

cpp 复制代码
#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 左值引用

使用完美转发

cpp 复制代码
#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 如何理解参数包

cpp 复制代码
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...运算符用来计算参数包中参数的个数。

cpp 复制代码
#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 包扩展

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

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

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

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

cpp 复制代码
#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

cpp 复制代码
#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

cpp 复制代码
#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

cpp 复制代码
#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系列的区别

cpp 复制代码
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 系列通常需要先构造一个完整对象再插入。

因此优势最明显。

面试高频总结

  1. push_back 与 emplace_back 的本质区别是什么?

    push_back 插入对象,emplace_back 构造对象。

  2. emplace_back 一定比 push_back 快吗?

    不一定。对象已经存在时,两者通常没有明显区别。

  3. 什么时候应该优先使用 emplace_back?

    当需要根据参数现场构造对象时。

  4. emplace_back 底层依赖什么技术?

    可变参数模板、完美转发和原地构造。

  5. 为什么 STL 后来增加了 emplace 系列接口?

    为了减少不必要的临时对象,提高构造复杂对象时的效率。

相关推荐
星恒随风1 小时前
C++ 内存管理详解:从内存分区、malloc/free 到 new/delete
开发语言·c++·笔记·学习
object not found1 小时前
Node.js fs 常用 API 整理:node:fs/promises、node:fs、fs 到底怎么用
开发语言·前端·javascript
C+++Python1 小时前
C++ 常量全面讲解
java·开发语言·c++
江屿风1 小时前
C++图论基础拓扑排序经典OJ题流食般投喂
开发语言·c++·笔记·算法·图论
芯岭技术郦1 小时前
MS32C001‑C:极致成本 32 位 MCU
c语言·开发语言·单片机
C+-C资深大佬1 小时前
C++ 数字与字符串互转
java·c++·算法
nexustech1 小时前
simplejson:Python JSON 处理的备用引擎
开发语言·python·其他·json
雷工笔记1 小时前
MES系列48-MES 系统「质量管理」完整设计与实施方案
开发语言·javascript·ecmascript
小灰灰搞电子1 小时前
C++ boost::asio 详解:网络编程领域的“瑞士军刀“
网络·c++·boost