C++11(中):可变参数模板将成为重中之重

文章目录


前言

前面我们重点学习了C++11中的基础语法,重点在于左值与右值的认识以及它们的作用,区分左值与右值能让我们在程序运行时减少拷贝,大大地提高效率。在程序编写过程中,如何提高效率永远是第一目标。


提示:以下是本篇文章正文内容,下面案例可供参考

1️⃣一、类型分类(基本概念)

  • C++11以后,进⼀步对类型进行了划分,右值被划分纯右值(pure value,简称prvalue)和将亡值(expiring value,简称xvalue)
  • 纯右值是指那些字面值常量或求值结果相当于字面值或是⼀个不具名的临时对象。如:42、true、nullptr或者类似str.substr(1,2)、str1 + str2传值返回函数调用,或者整形a、b,a++,a+b等。纯右值和将亡值是在C++11提出的,C++11中的纯右值概念划分等价于C++98中的右值
  • 将亡值是指返回右值引用的函数的调用表达式和转换为右值引用的转换函数的调用表达,如move(x)、static_cast<X&&>(x)
  • 泛左值(generalized value,简称glvalue),泛左值包含将亡值和左值。

2️⃣二、引用折叠(重点)

2.1引用折叠规则

  • C++中不能直接定义引用的引用如int& && r = i;这样会报错,通过模板或typedef中的类型操作可以构成引用的引用
  • 通过模板或 typedef 中的类型操作可以构成引⽤的引⽤时,这时C++11给出了⼀个引⽤折叠的规则:右值引⽤的右值引⽤折叠成右值引⽤,所有其他组合均折叠成左值引⽤
  • 下⾯的程序中很好的展⽰了模板和typedef时构成引⽤的引⽤时的引⽤折叠规则,⼤家需要⼀个⼀个仔细理解⼀下
  • 像f2这样的函数模板中,T&& x参数看起来是右值引⽤参数,但是由于引⽤折叠的规则,他传递左值时就是左值引⽤,传递右值时就是右值引⽤,有些地⽅也把这种函数模板的参数叫做万能引⽤
  • Function(T&& t)函数模板程序中,假设实参是int右值,模板参数T的推导int,实参是int左值,模板参数T的推导int&,再结合引⽤折叠规则,就实现了实参是左值,实例化出左值引⽤版本形参的Function,实参是右值,实例化出右值引⽤版本形参的Function
cpp 复制代码
template<class T>
void f1(T& x)
{ }

template<class T>
void f2(T&& x)
{ }


int main()
{
	typedef int& lref;
	typedef int&& rref;
	int n = 0;

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

	//没有形成折叠
	f1<int>(n);//n不是引用类型,没有构成引用折叠,实例化为->void f1(int& x)
	//f1<int>(0);//报错,右值不能传给左值引用

	//形成折叠->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);//右值可以传给const左值引用

	//形成折叠->void f1(const int& x)
	f1<const int&&>(n);
	f1<const int&&>(0);

	//没有折叠->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);

	return 0;
}

2.2万能引用

cpp 复制代码
template<class T>
void function(T&& t)//万能引用
{
	int a = 0;
	T x = a;
	cout << &a << endl;
	cout << &x << endl << endl;
}
int main()
{
	function(10);//10是右值,T被实例化为int,void function(int&& t)

	int a;
	function(a);//a是左值,T被实例化为int&,int&与&&形成引用折叠,void function(int& x)

	function(move(a));//move(a)是右值,引用折叠为void function(int&& x)
	const int b = 8;
	function(b);//b是const左值,引用折叠为void function(const int& x)

  function(std::move(b));//move(b)是右值,引用折叠为void function(const int&& x)
}

3️⃣三、完美转发

3.1前景提要

还记得上一篇文章讲的一个点吗?

右值引用的变量,其属性是左值引用

这就带来了一个问题:

cpp 复制代码
void func(string& s) { cout << "左值版本: " << s << endl; }
void func(string&& s) { cout << "右值版本: " << move(s) << endl; }

template <typename T>
void wrapper(T&& t) {
    func(t); // 即使传入右值,t也是左值,永远调用左值版func
}

int main() {
    string s = "hello";
    wrapper(s); // 正常
    wrapper(string("world")); // 本应调用右值版,实际调用左值版 → 不符合预期
}
cpp 复制代码
运行结果:
"左值版本: "
"左值版本: "

问题根源:万能引用接收的参数,在函数内是具名对象(左值),直接传递会丢失原始右值属性,完美转发就是为了解决这个 "属性丢失" 问题。


3.2完美转发的核心实现:std::forward

std::forward是实现完美转发的唯一工具,本质是带条件的类型转换------ 根据模板推导的类型,将参数还原为原始的左 / 右值类型,结合万能引用即可实现无损耗转发。

cpp 复制代码
// 完美转发的包装函数(仅修改这一行)
void func(string& s) { cout << "左值版本: " << s << endl; }
void func(string&& s) { cout << "右值版本: " << move(s) << endl; }

template <typename T>
void wrapper(T&& t) {
    func(std::forward<T>(t)); // 还原参数原始类型,传入的是右值,t也会转发为右值
}

int main() {
    string s = "hello";
    wrapper(s); // 转发左值 → 调用左值版:左值版本: hello
    wrapper(string("world")); // 转发右值 → 调用右值版:右值版本: world
}
cpp 复制代码
运行结果:
"左值版本: "
"右值版本: "

4️⃣四、可变参数模板

4.1基本语法及原理

  • C++11⽀持可变参数模板,也就是说⽀持可变数量参数的函数模板和类模板,可变数⽬的参数被称为参数包,存在两种参数包:模板参数包,表⽰零或多个模板参数;函数参数包:表⽰零或多个函数参数
  • template <class ...Args> void Func(Args... args) {}
    template <class ...Args> void Func(Args&... args) {}
    template <class ...Args> void Func(Args&&... args) {}
  • 我们⽤省略号来指出⼀个模板参数或函数参数的表示⼀个包,在模板参数列表中,class...或typename...指出接下来的参数表⽰零或多个类型列表;在函数参数列表中,类型名后⾯跟...指出接下来表示零或多个形参对象列表;函数参数包可以用左值引⽤或右值引⽤表⽰,跟前⾯普通模板⼀样,每个参数实例化时遵循引⽤折叠规则
  • 可变参数模板的原理跟模板类似,本质还是去实例化对应类型和个数的多个函数
  • 这⾥我们可以使⽤sizeof...运算符去计算参数包中参数的个数
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.1, string("xxxxx"), x); // 包⾥有3个参数
    return 0;
}
cpp 复制代码
运行结果
0
1
2
3
cpp 复制代码
// 原理1:编译本质这⾥会结合引⽤折叠规则实例化出以下四个函数
void Print();
void Print(int&& arg1);
void Print(int&& arg1, string&& arg2);
void Print(double&& arg1, string&& arg2, double& arg3);
// 原理2:更本质去看没有可变参数模板,我们实现出这样的多个函数模板才能⽀持
// 这⾥的功能,有了可变参数模板,我们进⼀步被解放,他是类型泛化基础
// 上叠加数量变化,让我们泛型编程更灵活。
void Print();
template <class T1>
void Print(T1&& arg1);
template <class T1, class T2>
void Print(T1&& arg1, T2&& arg2);
template <class T1, class T2, class T3>
void Print(T1&& arg1, T2&& arg2, T3&& arg3);

4.2包扩展

  • 对于⼀个参数包,我们除了能计算他的参数个数,我们能做的唯⼀的事情就是扩展它,当扩展⼀个包时,我们还要提供⽤于每个扩展元素的模式,扩展⼀个包就是将它分解为构成的元素,对每个元素应⽤模式,获得扩展后的列表。我们通过在模式的右边放⼀个省略号(...)来触发扩展操作。底层的实现细节如图1所⽰。
  • C++还⽀持更复杂的包扩展,直接将参数包依次展开依次作为实参给⼀个函数去处理。
cpp 复制代码
// 编译器递归的终止条件,参数包是0个时,直接匹配这个函数
void ShowList()
{
    cout << endl;
}

template <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;
}

实现过程如下:

cpp 复制代码
// 注释掉的原始可变参数模板版本
//template <class T, class ...Args>
//void ShowList(T x, Args... args)
//{
// cout << x << " ";
// Print(args...);
//}

// Print(1, string("xxxxx"), 2.2);调用时
// 本质编译器将可变参数模板通过模式的包扩展,推导生成以下三个重载函数
void ShowList(double x)
{
 cout << x << " ";
 ShowList();
}

void ShowList(string x, double z)
{
 cout << x << " ";
 ShowList(z);
}

void ShowList(int x, string y, double z)
{
 cout << x << " ";
 ShowList(y, z);
}

void Print(int x, string y, double z)
{
 ShowList(x, y, z);
}

总结

今天我们重点学习了折叠引用和可变参数模板,这两者在今后的语法使用中会占大头,大家务必认真学习,多敲才能其意自现。

相关推荐
凯子坚持 c2 小时前
C++基于微服务脚手架的视频点播系统---客户端(2)
开发语言·c++·微服务
Vivienne_ChenW2 小时前
Spring 事件驱动用法总结
java·开发语言·spring boot·spring
Beginner x_u2 小时前
JavaScript 中浅拷贝与深拷贝的差异与实现方式整理
开发语言·javascript·浅拷贝·深拷贝
柯一梦2 小时前
STL2--vector的介绍以及使用
开发语言·c++
txinyu的博客2 小时前
解析muduo源码之 EPollPoller.h & EPollPoller.cc
c++
云霄IT2 小时前
go语言post请求遭遇403反爬解决tls/ja3指纹或Cloudflare防护
开发语言·后端·golang
自动化控制仿真经验汇总2 小时前
电子抑振控制实验中MATLAB+示波器的用法-PART-RIGOL-电磁制振
开发语言·matlab
凯子坚持 c2 小时前
C++基于微服务脚手架的视频点播系统---客户端(3)
开发语言·c++·微服务
代码方舟2 小时前
Java后端实战:对接天远车辆过户查询API打造自动化车况评估系统
java·开发语言·自动化