
🎬 个人主页 :MSTcheng · CSDN
🌱 代码仓库 :MSTcheng · Gitee
🔥 精选专栏 : 《C语言》
《数据结构》
《算法学习》
《C++由浅入深》
💬座右铭: 路虽远行则将至,事虽难做则必成!
前言:在上一篇文章中我们向大家介绍了,C++11中的列表初始化{},以及右值引用和移动语义。本篇文章我们就接着上一篇的内容来讲,主要介绍类型分类,引用折叠,完美转发,以及可变模板参数。
文章目录
一、类型分类
C++11对类型进行了分类,前面我们介绍了左值和右值那么划分以后,右值就被划分纯右值,和将亡值;左值则被包含在了泛左值。
- 纯右值 :指那些字面值常量或求值结果相当于字面值或是一个不具名的临时对象。如:
42、true、nullptr或者类似str.substr(1, 2)、str1 + str2传值返回函数调用,或者整形a、b,a++,a+b等 - 将亡值 :指返回右值引用的函数的调用表达式和转换为右值引用的转换函数的表达式如:
move(x)、static_cast<X&&>(x)。、 - 泛左值 :包含将亡值和左值。

二、引用折叠
在前面的文章中我们讲解了左值引用和右值引用,左值引用引用的是一个左值也可能是一个右值,但右值引用引用的一定是一个右值。但是右值引用引用了右值后,本身就拥有了左值属性。那如果我们想引用左值引用或右值引用怎么办呢?引用折叠------解决引用的引用问题。
-
C++中不能直接定义引用的引用,比如
int i=0; int& p=i; int & && r=p这样的定义是错误的因为int&是左值引用int&&是右值引用,int& &&编译器推不出到底是哪种引用于是就推出了引用折叠。此外除了引用折叠能解决引用的引用,使用typedef也可以解决。cppint main() { typedef int& lref; typedef int&& rref; int n = 0; lref& r1 = n; // r1 的类型是 int&(左值引用int&+&=左值引用) lref&& r2 = n; // r2 的类型是 int&(左值引用int&+&&=左值引用) rref& r3 = n; // r3 的类型是 int&(右值引用int&&+&=左值引用) rref&& r4 = 1; // r4 的类型是 int&&(右值引用int&&+&&=右值引用) return 0; }
从上面我们可以得出一些结论 :当 左值引用 碰到 左值引用 最终的类型为 左值引用,当 左值引用 碰到 右值引用 最终为 左值引用,当 右值引用 碰到 左值引用 折叠后最终为 左值引用 ,当右值引用碰到右值引用折叠后最终结果才为右值引用!!!
下面来看一段代码:
cpp
//由于引用折叠限定,f1实例化以后总是一个左值引用
template<class T>
void f1(T& x)
{}
// 由于引用折叠限定,f2实例化后可以是左值引用,也可以是右值引用,万能引用
template<class T>
void f2(T&& x)
{}
int main()
{
typedef int& lref;
//typedef int&& rref;
using rref = int&&;
int n = 0;//n是一个左值
lref& r1 = n; // r1 的类型是 int&
lref&& r2 = n; // r2 的类型是 int&
rref& r3 = n; // r3 的类型是 int&
rref&& r4 = 1; // r4 的类型是 int&&
// 没有折叠->实例化为void f1(int& x)
f1<int>(n);
//f1<int>(0); // 报错 f1的T被推导成int 此时就是左值引用 左值引用引用不了右值所以报错
// 折叠->实例化为void f1(int& x)
f1<int&>(n);
//f1<int&>(0); // 报错 f1的T被推导成int& int&+&=int& 左值引用碰上左值引用=左值引用 引用不了右值所以报错
// 折叠->实例化为void f1(int& x)
f1<int&&>(n);
//f1<int&&>(0); // 报错 f1的T被推导成int&&右值引用 int&&+&=int& 左值引用引用不了右值报错
// 折叠->实例化为void f1(const int& x)
f1<const int&>(n); //cont左值既可以接收左值也可以接收右值 不会报错
f1<const int&>(0);
// 折叠->实例化为void f1(const int& x)
f1<const int&&>(n); //const 右值(int&&) + & =const int& 相当于const左值 既可以引用左值又可以引用右值
f1<const int&&>(0);
// 没有折叠->实例化为void f2(int&& x)
//f2<int>(n); //报错 f2的T推导成 int&& 右值引用 只能接收右值不能接收左值所以报错
f2<int>(0);
// 折叠->实例化为void f2(int& x)
f2<int&>(n);
//f2<int&>(0); // 报错 f2的T推导成int& int&+&&=int& 左值引用 不能引用右值所以报错
// 折叠->实例化为void f2(int&& x)
//f2<int&&>(n); // 报错 f2的T推到成int&& + &&=int&& 右值引用 不能引用左值所以报错
f2<int&&>(0);
return 0;
}
通过上面的代码我们认识到了两个规律:
1、如果函数的参数为左值引用比如
f1(T &x),那么此时无论T是左值引用(int&)还是右值引用(int&&),再加上一个&,最终都会变成左值引用。 除非T是const int&,但是由于const左值引用不允许修改,而右值引用的初衷是支持右值能够修改能够移动它的资源的,而加上const是违背初衷的,所以const左值这种情况我们忽略。
2、而如果我们将函数的参数设计成像f2(T&& x)一样是右值引用的版本,那么就既可以传左值也可以传右值了 ,且当且仅当T被推导成int&&(以int来举例)时,此时才是右值引用,其余的都是左值引用。 这样的右值作为形参类型的引用我们称为万能引用,因为它既能引用左值的引用又能引用右值的引用。
下面来看一段代码判断一下是否是万能引用?
cpp
void push_back(const T& x)
{
insert(end(), x);
}
void push_back(T&& x)
{
insert(end(), move(x));
//insert(end(), (T&&)x);
}
这是我们从链表部分拿出来的一个
push_back接口,以前我们看到的都是上面那个const T&左值的版本,在上面的代码中我们看到了将形参类型设计成右值传参就是万能引用,在这里我们也将push_back接口设计成右值引用,那他是否是万能引用呢?
- 很显然,这当然不是,因为这是类模板,
T的类型是在链表实例化的时候就确定了,不是在实参传递的时候确定的。而且上面f1和f2是一个函数模板,而这里的push_back并不是一个函数模板,所以这里右值引用的push_back仅仅是一个右值传参的版本。
那么如果我们像将它变成一个万能引用也很简单,让他变成函数模板就可以!
cpp
// 万能引用
template<class X>
void push_back(X&& x)
{
insert(end(), move(x));
}
int main()
{
my_list::list<bit::string> lt;
// 左值
my_string::string s1("111111111111111111111");
lt.push_back(s1);//push_back接收的就是一个左值
cout << "*************************" << endl;
//push_back接收的是一个右值
lt.push_back(bit::string("22222222222222222222222222222"));
cout << "*************************" << endl;
//push_back接收的是一个右值
lt.push_back("3333333333333333333333333333");
cout << "*************************" << endl;
return 0;
}
通过将我们自己实现的
list.h中的push_back修改成万能引用之后,push_back所推导的类型就是在形参传递的时候推导了,如果传的是一个左值那么就推导成左值引用来引用左值,如果是传递的是一个右值那么就推导成一个右值引用来引用右值。
cpp
void push_back(T&& x)
{
insert(end(), move(x));
//insert(end(), (T&&)x);
}
到这里其实还有一个问题就是: 在push_back的接口中,我们最终是去move()了一下x,前面说过move函数就是强转成右值引用的类型,那么我们通过push_back接口所传入的x经过move了之后全部都变成了右值引用,那就区分不了左值和右值了。
因为万能引用设计出来就是为了既能引用左值的引用,又能引用右值的引用,但是无论传入的是一个左值的引用还是右值的引用,最终都通过
move变成了右值引用,这就无法让左值引用和右值引用去调用各自的函数了,那么这样的万能引用设计出来就没有什么意义了。
为了解决让传进来左值引用和右值引用能够分别调用自己版本的函数,C++11又提出了完美转发。
三、完美转发
为了区分万能引用所接收到的左值引用和右值引用,C++使用一个关键字forward,使得传入进来的参数能够保持它自己的属性,是左值引用就保持左值引用的属性往下传递,是右值引用就保持右值引用的属性往下传递,这样他们就能分别调用到适合自身版本的函数了。
下面来看代码:
cpp
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;
// a是左值,推导出T为int&,引⽤折叠,模板实例化为void Function(int& t)
Function(a); // 左值
// std::move(a)是右值,推导出T为int,模板实例化为void Function(int&& t)
Function(std::move(a)); // 右值
const int b = 8;
// a是左值,推导出T为const int&,引⽤折叠,模板实例化为void Function(const int&t)
Function(b); // const 左值
// std::move(b)右值,推导出T为const int,模板实例化为void Function(const int&&t)
Function(std::move(b)); // const 右值
return 0;
}

通过上面我们可以看到,通过万能引用里面的
forward之后,通过万能引用传入的参数保持了它原有的属性,左值引用就保持左值引用的属性,然后调用左值引用的函数; 右值引用就保持右值引用的属性,然后调用右值引用的函数。
Function(T&& t)函数模板程序中,传左值实例化以后是左值引用的Function函数,传右值实例化以后是右值引用的Function函数。
cpp
template <class T> T&& forward (typename remove_reference<T>::type&arg);
template <class T> T&& forward (typename remove_reference<T>::type&& arg)
- 完美转发
forward本质是一个函数模板 ,他主要还是通过引用折叠的方式实现 ,下面示例中传递给Function的实参是右值 ,T被推导为int,没有折叠,forward内部t被强转为右值引用返回;传递给Function的实参是左值,T被推导为int&,引用折叠为左值引用,forward内部t被强转为左值引用返回
所以我们上面的push_back函数应该加上forward使得传入的x保持其原有的特性:

四、可变参数模板
4.1基本语法以及原理
在之前学习模板的时候我们接触到的都是确定的参数模板,不能根据形参类型来推出模板类型从而实例化出一个最匹配的函数。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...指出接下来的参数表示零或多个类型列表 ;在函数参数列表中,类型名后面跟...指出接下来表示零或多个形参对象列表;函数参数包可以用左值引用或右值引用表示,跟前面普通模板⼀样,每个参数实例化时遵循引用折叠规则。
下面来看一个例子:
cpp
template <class ...Args>
void Print(Args&&... args)
{
//使用sizeof去计算参数包里的个数
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;
}


4.2包扩展
-
对于⼀个参数包,我们除了能计算他的参数个数,我们能做的唯⼀的事情就是扩展它 ,当扩展⼀个包时,我们还要提供⽤于每个扩展元素的模式,扩展⼀个包就是将它分解为构成的元素,对每个元素应⽤模式,获得扩展后的列表。我们通过在模式的右边放⼀个省略号
(...)来触发扩展操作。 举个例子:cpptemplate <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...); } int main() { Print(); Print(1); Print(1, string("xxxxx")); Print(1, string("xxxxx"), 2.2); return 0; } ```
我们首先定义了一个
ShowList的可变参数模板,当我们在ShowList函数,而在ShowList函数里面去调用它自己就是我们对于包的扩展,这是一个逐层的递归调用,具体原理如下:

通过以上的结果我们可以总结规律,在调用
ShowList函数的时候,先调用三个参数的ShowList函数,然后再调用2个参数的,最后结束调用无参的ShowList。
为什么会这样递归调用?
因为
ShowList函数都会根据实参实例化出不同的函数,而ShowList函数扩展了让它自己调用了自己,所以它实例化的函数是不确定的,所以需要逐层传参来实例化出不同的函数!
五、empalce系列接口
要说可变模板参数有什么作用,empalce系列接口就是使用可变模板参数来实现的。C++11以后STL容器新增了empalce系列的接口,empalce系列的接口均为模板可变参数,功能上兼容push和insert系列。具体的结构如下:
cpp
template <class... Args> void emplace_back (Args&&... args);
template <class... Args> iterator emplace (const_iterator position,
Args&&... args);
但是empalce还支持新玩法,假设容器为container<T>,empalce还支持直接插入构造T对象的参数,这样有些场景会更高效一些,可以直接在容器空间上构造T对象。这里我们改造一下我们以前实现list的代码:
cpp
namespace my_list
{
template<class T>
struct list_node
{
list_node* _next;
list_node* _prev;
T _data;
list_node(const T& x)
:_next(nullptr)
, _prev(nullptr)
, _data(x)
{
}
list_node(T&& x = T())
:_next(nullptr)
, _prev(nullptr)
, _data(move(x))
{
}
template <class... Args>
list_node(Args&&... args)
: _next(nullptr)
, _prev(nullptr)
, _data(forward<Args>(args)...)
{
}
};
template<class T>
class list
{
typedef list_node<T> Node;
public:
typedef list_iterator<T, T&, T*> iterator;
//typedef list_const_iterator<T> const_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);
}
}
// lt2(lt1)
//list(const list<T>& lt)
list(list<T>& lt)
{
empty_init();
for (auto& e : lt)
{
push_back(e);
}
}
void swap(list<T>& lt)
{
std::swap(_head, lt._head);
std::swap(_size, lt._size);
}
// lt1 = lt3
list<T>& operator=(list<T> lt)
{
swap(lt);
return *this;
}
~list()
{
clear();
delete _head;
_head = nullptr;
}
void clear()
{
auto it = begin();
while (it != end())
{
it = erase(it);
}
}
size_t size()
{
return _size;
}
//==================
//左值版本的push_back
//==================
void push_back(const T& x)
{
/*Node* tail = _head->_prev;
Node* newnode = new Node(x);
tail->_next = newnode;
newnode->_prev = tail;
newnode->_next = _head;
_head->_prev = newnode;*/
insert(end(), x);
}
// 万能引用
template<class X>
void push_back(X&& x)
{
// 保持他的属性,往下传递,左值引用就是保持左值属性,右值引用就保持右值属性
insert(end(), forward<X>(x));
}
//插入版本的万能引用
//template<class X>
//void insert(iterator pos, X&& x)
//{
// Node* cur = pos._node;
// Node* prev = cur->_prev;
// Node* newnode = new Node(forward<X>(x));
// // prev newnode cur;
// prev->_next = newnode;
// newnode->_prev = prev;
// newnode->_next = cur;
// cur->_prev = newnode;
// ++_size;
//}
//使用可变模板参数实现empalce接口
template <class... Args>
void emplace_back(Args&&... args)
{
emplace(end(), forward<Args>(args)...);
}
//实现在pos位置插入的empalce接口
template <class... Args>
void emplace(iterator pos, Args&&... args)
{
Node* cur = pos._node;
Node* prev = cur->_prev;
Node* newnode = new Node(forward<Args>(args)...);
// prev newnode cur
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = cur;
cur->_prev = newnode;
++_size;
}
//左值引用版本的插入
void insert(iterator pos, const T& x)
{
Node* cur = pos._node;
Node* prev = cur->_prev;
Node* newnode = new Node(x);
// prev newnode cur
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = cur;
cur->_prev = newnode;
++_size;
}
//右值引用版本的插入
void insert(iterator pos, T&& x)
{
Node* cur = pos._node;
Node* prev = cur->_prev;
Node* newnode = new Node(move(x));
// prev newnode cur
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = cur;
cur->_prev = newnode;
++_size;
}
iterator erase(iterator pos)
{
assert(pos != end());
Node* cur = pos._node;
Node* prev = cur->_prev;
Node* next = cur->_next;
prev->_next = next;
next->_prev = prev;
delete cur;
--_size;
//return iterator(next);
return next;
}
private:
Node* _head;
size_t _size;
};
我们模拟实现了
list的emplace和emplace_back接口,这里把参数包不段往下传递,最终在结点的构造中直接去匹配容器存储的数据类型T的构造,所以达到了前⾯说的empalce支持直接插入构造T对象的参数,这样有些场景会更高效一些,可以直接在容器空间上构造T对象。
注意:
传递参数包过程中,如果是 Args&&... args 的参数包,要用完美转发参数包,方式如下std::forward<Args>(args)... ,否则编译时包扩展后右值引⽤变量表达式就变成了左值。
cpp
#include"list.h"
#include"string.h"
#include<iostream>
using namespace std;
int main()
{
my_list::list<my_string::string> lt;
// 传左值,跟push_back一样,走拷贝构造
my_string::string s1("111111111111");
lt.emplace_back(s1);
cout << "*********************************" << endl;
// 右值,跟push_back一样,走移动构造
lt.emplace_back(move(s1));
cout << "*********************************" << endl;
// 直接把构造string参数包往下传,直接用string参数包构造string
// 这里达到的效果是push_back做不到的
lt.emplace_back("111111111111"); // 构造
lt.emplace_back(10, 'x'); // 构造
lt.push_back("111111111111"); // 构造+移动构造
cout << "*********************************" << endl;
// 链表中存日期类对象
//lt.emplace_back(2025, 1, 1); // 构造
//lt.push_back({ 2025, 1, 1 }); // 构造+拷贝构造
my_list::list<pair<my_string::string, int>> lt1;
// 跟push_back一样
// 构造pair + 拷贝/移动构造pair到list的节点中data上
pair<my_string::string, int> kv("苹果", 1);
lt1.emplace_back(kv);
cout << "*********************************" << endl;
// 跟push_back一样
lt1.emplace_back(move(kv));
cout << "*********************************" << endl;
////////////////////////////////////////////////////////////////////
// 直接把构造pair参数包往下传,直接用pair参数包构造pair
// 这里达到的效果是push_back做不到的
lt1.emplace_back("苹果", 1);
// lt1.emplace_back({ "苹果", 1 }); 不支持,形参是模板,无法推导形参类型
lt1.push_back({ "苹果", 1 }); // 隐式类型转换传参
cout << "*********************************" << endl;
return 0;
}

总结:
总体而言,综合各个场景,比较推荐empalce系列接口,在深拷贝的时候emplace的直接构造和push_back的移动构造没有什么差别,但是对于浅拷贝empalce效率就比较高了。所以empalce系列的综合效率更高更推荐使用构造容器存储对象的参数包,去插入,这里可以实现直接构造,效率更高。
html
MSTcheng 始终坚持用直观图解 + 实战代码,把复杂技术拆解得明明白白!
👁️ 【关注】 看普通程序员如何用实用派思路搞定复杂需求
👍 【点赞】 给 "不搞虚的" 技术分享多份认可
🔖 【收藏】 把这些 "好用又好懂" 的干货技巧存进你的知识库
💬 【评论】 来唠唠 ------ 你踩过最 "离谱" 的技术坑是啥?
🔄 【转发】把实用技术干货分享给身边有需要的程序员伙伴
技术从无唯一解,让我们一起用最接地气的方式,写出最扎实的代码! 🚀💻
能够看到这里的小伙伴已经打败95%的人了超棒的,为你点赞,休息一下吧!
