C++现代模板编程核心技术精解:从类型分类、引用折叠、完美转发的内在原理,到可变模板参数的基本语法、包扩展机制及emplace接口的底层实现

🔥个人主页:胡萝卜3.0****

📖个人专栏:************************************************************************************************************************************************************************************************************************************************************《C语言》、《数据结构》 、《C++干货分享》、LeetCode&牛客代码强化刷题****************************************************************************************************************************************************************************************************************************************************************

《Linux系统编程》

⭐️人生格言:不试试怎么知道自己行不行


🎥胡萝卜3.0🌸的简介:


目录

前沿

[3.8 类型分类](#3.8 类型分类)

[3.9 引用折叠](#3.9 引用折叠)

[3.10 完美转发](#3.10 完美转发)

四、可变模板参数

[4.1 基本语法及原理](#4.1 基本语法及原理)

[4.2 包扩展(了解)](#4.2 包扩展(了解))

[4.3 emplace 接口](#4.3 emplace 接口)

结尾


前沿

在前面的学习中,我们学习了C++11中的两大重要特性:列表初始化和右值引用。列表初始化统一了对象的初始化方式,支持内置类型和自定义类型,通过std::initializer_list实现容器初始化。右值引用区分左值和右值,通过移动语义提高效率,解决了传值返回的拷贝问题。

接下来,我们接着学习~~~

3.8 类型分类

C++11以后,进一步对类型进行了划分,右值被划分成:

  • 纯右值(pure value,简称prvalue)
  • 将亡值(expiring value,简称xvalue)

纯右值是指那些字面量常量或求值结果相当于字面值或是一个不具名的临时对象......(临时对象、匿名对象、字面量......)。如:42、true、nullptr或者类似str.substr(1,2)、str1+str2传值返回函数调用,或者整型a,b,a+b......等。

将亡值是指返回右值引用的函数的调用表达式和转换为右值引用的转换函数的调用表达,如move(x)、static_cast<X&&>(x)

纯右值和将亡值是在C++11中提出的,C++11中纯右值概念划分等价于C++98中的右值

泛左值(glvalue),泛左值包含将亡值和左值

Value categories - cppreference.com这是关于值类型的英文的官方文档,有兴趣可以了解一下。

说实话,这个分类并没有什么意义,了解即可~

接下来要介绍的东西非常重要!!!

3.9 引用折叠

所谓引用折叠就是------引用再遇到引用的时候就叫做引用折叠

但是在C++中我们是不能直接定义引用的引用,如int& && r=i;这样写会直接报错

虽然我们不能直接定义引用的引用的,但是我们可以间接的------

  • 通过模板、typedef 或者using 中的类型操作可以构成引用的引用

那这里有个问题:不是说引用折叠吗?这个体现在哪里呢?

ok,这个要体现在引用对象的变量的类型上!!!

我们通过模板或者typedef中的类型操作可以构成引用的引用时,这时C++11给出了一个引用折叠的规则:右值引用的右值引用折叠成右值引用,所有其他组合均折叠成左值引用。

也就是说------

ok,我们通过下面的程序可以很好的展示模板和typedef 时构造引用的引用时的引用折叠规则,我们来一个一个的仔细理解一下------

这里为了更好的看出区别,我们就进行显示模板参数------

cpp 复制代码
template<class T>
void f1(T& x)
{}
int main()
{
	typedef int& lref;
	typedef int&& rref;
	int n = 0;//n是一个左值
	
	f1<int>(n);
	f1<int>(0); // 报错
	
	f1<int&>(n);
	f1<int&>(0); // 报错

	f1<int&&>(n);
	f1<int&&>(0); // 报错

	f1<const int&>(n);
	f1<const int&>(0);

	f1<const int&&>(n);
}

我们运行一下上面的代码------

这是为什么?ok,这肯定是和上面的引用折叠有关系------我们一起来看一下

不知道有没有uu发现了什么?我们这里的模板函数的参数是一个左值引用,由于这里引用折叠的限定,f1实例化后总是一个左值引用

ok,这是一个模板函数的参数是左值引用的情况,接下来我们来看一下模板函数的参数是右值引用的情况------

cpp 复制代码
template<class T>
void f2(T&& x)
{

}
int main()
{
	typedef int& lref;
	typedef int&& rref;
	int n = 0;//n是一个左值

	f2<int>(n); // 报错
	f2<int>(0);
	
	f2<int&>(n);
	f2<int&>(0); // 报错
	
	f2<int&&>(n); // 报错
	f2<int&&>(0);
	return 0;
}

当我们运行上面的代码时,还是会出现和上面一样的错误,这里就不过于展示。

那我们就来分析一下------

我们这里的模板函数的参数是一个右值引用,由于这里引用折叠的限定,f1实例化后可以是一个左值引用,也可以是一个右值引用

接下来,我们再来看一个函数模板参数是右值引用的比较实际的例子------

cpp 复制代码
template<class T>
void Function(T&& t)
{
	int a = 0;
	T x = a;
	//x++;
	cout << &a << endl;
	cout << &x << endl << endl;
} 
int main()
{
	Function(10);

	int a;
	Function(a); 
	Function(std::move(a));

	const int b = 8;
	Function(b); 
	Function(std::move(b));
	return 0;
}

运行一下------

ok,在这里我们就要先知道一下模板参数是右值引用时的推导规则:

ok,现在我们来分析一下上面的代码------

那我们以后再看到这个模板参数的类型是右值引用的时候,我们就要给它换个名字了------万能引用

那既然有了这个万能引用,那我们是不是就可以解决在前面的学习中,我们既要写一个左值版本的push_back,又要写一个右值版本的push_back的问题------

这时,我就可以直接写出一个函数模板,这个函数模板不是代表我是左值引用,还是右值引用,而是都可以。

  • 你传左值参数,我就是左值引用;
  • 你传右值参数,我就是右值引用

那这时就有uu会说,那我们是不是直接保留第2个即可------

其实并不是,为什么不是呢?这个不是右值引用作为参数的类型吗?

而我们上面的万能引用是这么写的------

所以,这里我们要写成模板------

但是,这里改完之后会有新的问题,我们这里能不能进行------

ok,其实这里是不能将x进行move的操作的,move是不管x是左值还是右值都会强转成右值,那强转成右值,不又去调用右值的insert吗?和我们期待的结果不一样

  • 我们期待的是右值引用保持右值引用的属性,左值引用正常保持左值引用的属性

ok,那我们这里就要借助下面的一个非常完美的东西------完美转发!!!

3.10 完美转发

Function(T&& t)函数模板程序中,传左值实例化以后是左值引用的Function函数,传右值实例化以后是右值引用的Function函数。

forward可以识别是左值引用还是右值引用,如果是左值引用,就保持左值的属性;如果是右值引用(属性退化成左值),会再把你转换成右值,保持你的属性

完美转发forward本质是一个函数模板,他主要还是通过引用折叠的方式实现,我们来看一下这个例子------

cpp 复制代码
template <class _Ty>
_Ty&& forward(remove_reference_t<_Ty>& _Arg) noexcept
{ // forward an lvalue as either an lvalue or an rvalue
	return static_cast<_Ty&&>(_Arg);
} 
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;
}

运行结果------

结合我们在前面介绍过的内容:变量表达式都是左值属性也就意味着一个右值被右值引用绑定后,右值引用变量表达式的属性是左值,也就是说Function函数中t的属性是左值,那么我们把 t 传递给下一层函数Fun,那么匹配的都是左值引用版本的Fun函数。

这里我们想要保持t对象的属性,也就是说------

  • 如果是左值引用,就保持左值的属性
  • 如果是右值引用(右值引用变量表达式的属性是左值),会再把你转换成右值,保持你右值的属性

就需要使用完美转发实现。

  • 加上forward之后完整代码------
cpp 复制代码
template <class _Ty>
_Ty&& forward(remove_reference_t<_Ty>& _Arg) noexcept
{ // forward an lvalue as either an lvalue or an rvalue
	return static_cast<_Ty&&>(_Arg);
}
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(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被推导为int,没有折叠,右值引用变量表达式 t 的属性是左值,forward内部将t的属性强转成右值引用返回;传递给Function的实参是左值,T被推导成int&,引用折叠为void Function(int& x),forward内部将 t 强转成左值引用返回

注意:forward需要显示实例化!!!

ok,这样的话,我们就可以对上面的push_back的函数进行改写------

cpp 复制代码
teplate<class T1>
void push_back(T1&& x)
{
	insert(end(), forward<T1>(x));
}

四、可变模板参数

4.1 基本语法及原理

其实在C++98中,我们就浅浅的学习了一下------

在C++ 98中,我们学习过printf

printf这里所展示的...就是表示可变

C++11支持可变参数模板,也就是说支持可变数量参数的函数模板和类模板,可变数目的参数被称为参数包,存在两种参数包:

  • 模板参数包,表示0或者多个模板参数(0~N个模板参数)
  • 函数参数包,表示0或者多个函数参数(0~N个函数参数)
cpp 复制代码
template <class ...Args> void Func(Args... args) {}
template <class ...Args> void Func(Args&... args) {}
template <class ...Args> void Func(Args&&... args) {}

我们用省略号 ... 来指出一个模板参数或函数参数的表示一个包:

  • 在模板参数中,class ... 或者 typename ... 指出接下来的参数表示0或多个类型列表
  • 在函数模板中,类型名后面跟...指出接下来表示0或者多个形参对象列表

函数参数包可以使用左值引用或者右值引用,跟前面普通模板一样,每个参数实例化时遵循引用折叠规则

代码演示:

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, string("xxxxxx"), x);//包中有3个参数
	return 0;
}

在这里我们可以使用 sizeof... 运算符去计算参数包中的参数个数

运行结果------

这里有个问题:可变模板参数的原理是什么?

可变模板参数的原理跟模板一样,本质还是去实例化对应类型和个数的多个函数

ok,这里的重点是需要我们去记忆一下这个可变模板参数的写法!!!

4.2 包扩展(了解)

C++11中的包扩展,我们了解即可,因为后面会有更加便捷的写法

所谓包扩展,就是包参数包中的内容解析出来

对于一个参数包,我们除了能计算他的参数个数,能做的唯一的事情就是扩展它,当扩展一个包时,我们还要提供用于每个扩展元素的模式,扩展一个包就是将它分解为构成的元素,对每个元素应用模式,获得扩展后的列表。我们通过在模式的右边放一个省略号(...)来触发扩展操作。底层的实现细节如下图所示。

C++还支持更复杂的包扩展,直接将参数包依次展开依次作为实参给一个函数去处理。

C++17的方法更简单------折叠表达式

cpp 复制代码
template<class ...Args>
void Print(Args... args)
{
	((cout << args << " "), ...);
}
int main()
{
	double x = 2.2;
	Print(1, string("xxxxxx"), x);
    return 0;
}

C++17中的方式就比C++11中的方式要简单的很多

4.3 emplace 接口

emplace系列就是一个可变参数包,万能引用!!!

其实emplace_back和push_back的没有很大的差别------

emplace还支持新的玩法,假设容器为container<T>,emplace还支持直接插入构造T对象的参数,这样有些场景会更高效,可以直接在容器空间上构造T对象

我在list中,直接插入一个string,对于emplace_back来说,就是直接构造;而对于push_back来说却是构造+移动构造。这是为什么?

当我们插入一个pair时,会不会有什么差别呢?

好像没有什么差别

但是emplace_back可以直接把构造pair参数包传下去,直接用pair参数包构造pair,这个效果push_back是做不到的

emplace_back可以传构造pair的参数包!!!!

push_back只能这么传------

  • 总结:

就比如说,对于一个日期类:

  • push_back
  • emplace_back

总结:emplace_back总体而言是更高效,推荐以后使用emplace系列替代insert和push系列

结尾

都看到这里啦!那请大佬不要忘记给博主来个"一键三连"哦!

૮₍ ˶ ˊ ᴥ ˋ˶₎ა

相关推荐
亭上秋和景清2 小时前
指针进阶:函数指针详解
开发语言·c++·算法
9ilk2 小时前
【C++】--- C++11
开发语言·c++·笔记·后端
FMRbpm2 小时前
队列练习--------最近的请求次数(LeetCode 933)
数据结构·c++·leetcode·新手入门
Codebee2 小时前
OODER图生代码框架:Java注解驱动的全栈实现与落地挑战
人工智能
biter down2 小时前
C++ 函数重载:从概念到编译原理
开发语言·c++
中冕—霍格沃兹软件开发测试2 小时前
测试用例库建设与管理方案
数据库·人工智能·科技·开源·测试用例·bug
TextIn智能文档云平台3 小时前
什么是多模态信息抽取,它和传统OCR有什么区别?
大数据·人工智能
Linux后台开发狮3 小时前
DeepSeek-R1 技术剖析
人工智能·机器学习
拾荒的小海螺3 小时前
开源项目:AI-Writer 小说 AI 生成器
人工智能