Modern Effective C++:item 1 理解模板类型推导

通用引用(万能引用)

通用引用通常表示为 T&&,其中 T 是一个模板参数类型(模板中的&&表示通用引用)。&& 虽然通常表示一个右值引用,但当T由模板参数推导而来时,其既可以绑定左值,又可以绑定右值。当 T 是一个模板参数类型,并且T是推导出来的类型,T&&才是通用引用。

万能引用 既可以接收左值,又可以接收右值。实参是左值,通用引用就是左值引用(引用折叠)实参是右值,通用引用就是右值引用。

(引用折叠 :当其传入参数为左值时,&&会折叠成&;当传入参数为右值时,&&不折叠照常接收)

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<typename T>
void PerfectForward(T&& t){Fun(t);
}
int main(){PerfectForward(10); // 右值int a;PerfectForward(a);     return 0;
}

(2)模板类型推导

理解auto的模板类型推导如果你不介意浏览少许伪代码,可以考虑像这样一个函数模板:

template<typename T>
voidf(ParamType param);

它的调用看起来像这样

f(expr);                        //使用表达式调用f

编译期间,编译器使用expr进行两个类型推导:一个是针对T的,另一个是针对ParamType。这两个类型通常不同,因为ParamType包含一些修饰,如const和引用修饰符。

对于模板函数:

template<typename T>
void f(ParamType param);

编译器需要完成两步类型推导:

1)推导 T 的类型

T是模板参数,当调用 f 函数并传递一个参数时,编译器会尝试根据该参数来确定 T 的具体类型。

如果调用 f(10),编译器会推断 T 是 int。

(2)推导 ParamType 的类型

ParamType 是函数参数的类型,它可以是 T 本身,也可以是对 T 进行了一些修饰后的类型,比如加上 const、&(引用)、*(指针)等。例如,假设定义如下函数模板

template<typename T>
void f(const T& param);

ParamType 是 const T&,T 的常量引用。如果调用 f(10),编译器会推断 T 是 int,ParamType 是 const int&。

template<typename T>
void f(const T& param) {// 函数体
}

调用示例1

int x = 10;
f(x);

编译器推导 T 为 int,ParamType 为 const int&。

调用示例 2

double y = 3.14;
f(y);

编译器推导 T 为 double,ParamType 为 const double&。

调用示例 3

std::string s = "hello";
f(s);

编译器推导 T 为 std::string,ParamType 为 const std::string&。

T 是模板参数,ParamType 是函数参数的实际类型,它可能是 T 本身,也可能是对 T 进行了一些修饰后的类型(如 const T&、T* 等)。编译器会根据你传递给函数的实际参数来推导 T 和 ParamType 的具体类型。

有三种情况:

  • ParamType是一个指针或引用,但不是通用引用
  • ParamType是一个通用引用
  • ParamType既不是指针也不是引用

分情况讨论三种情况,每个情景基于之前给出的模板:

template<typename T>
voidf(ParamType param);
f(expr);                        //从expr中推导T和ParamType

情景一:ParamType是一个指针或引用,但不是通用引用

在这种情况下,类型推导会这样进行:

  1. 如果expr的类型是一个引用,忽略引用部分。
  2. 然后expr的类型与ParamType进行模式匹配来决定T
template<typename T>
voidf(T& param);               //param是一个引用

我们声明这些变量,

int x=27;//x是int
const int cx=x;//cx是const int
const int& rx=x;//rx是指向作为const int的x的引用

在不同的调用中,对paramT推导的类型会是这样:

f(x);     //T是int,param的类型是int&
f(cx);    //T是const int,param的类型是const int&
f(rx);    //T是const int,param的类型是const int&

在第二个和第三个调用中,因为cxrx被指定为const值,所以T被推导为const int,从而产生了const int&的形参类型。这对于调用者来说很重要。当他们传递一个const对象给一个引用类型的形参时,他们期望对象保持不可改变性,也就是说,形参是reference-to-const的。

这也是为什么将一个const对象传递给以T&类型为形参的模板安全的:对象的const会被保留为T的一部分。

在第三个例子中,即使rx的类型是一个引用,T也会被推导为一个非引用 ,这是因为rx的引用性在类型推导中会被忽略。

如果我们将f的形参类型T&改为const T&,情况有所变化。cxrxconst依然被遵守,但是因为现在假设param是reference-to-constconst不再被推导为T的一部分:

template<typename T>
voidf(const T& param);//param现在是reference-to-constint x = 27;                     //如之前一样
const int cx = x;//如之前一样constint& rx = x; 
//如之前一样
f(x);    //T是int,param的类型是const int&
f(cx);   //T是int,param的类型是const int&
f(rx);   //T是int,param的类型是const int&

同之前一样,rx的reference-ness在类型推导中被忽略了。如果param是一个指针(或者指向const的指针)而不是引用,情况本质上也一样:

template<typename T>
voidf(T* param); //param现在是指针
int x = 27;//同之前一样
const int *px = &x;//px是指向作为const int的x的指针
f(&x);//T是int,param的类型是int*
f(px);//T是const int,param的类型是const int*

情景二:ParamType是一个通用引用

模板使用通用引用形参的话,那事情就不那么明显了。这样的形参被声明为像右值引用一样(也就是,在函数模板中假设有一个类型形参T,那么通用引用声明形式就是T&&),它们的行为在传入左值实参时大不相同。

  • 如果expr是左值,TParamType都会被推导为左值引用。第一,这是模板类型推导中唯一一种T被推导为引用的情况。第二,虽然ParamType被声明为右值引用类型,但是最后推导的结果是左值引用。
  • 如果expr是右值,就使用正常的推导规则
template<typename T>
voidf(T&& param); //param现在是一个通用引用类型
int x=27; //如之前一样
const int cx = x; //如之前一样
const int & rx =cx;//如之前一样
f(x);  //x是左值,所以T是int&,//param类型也是int&
f(cx); //cx是左值,所以T是const int&,//param类型也是const int&
f(rx); //rx是左值,所以T是const int&,//param类型也是const int&
f(27);   //27是右值,所以T是int,param类型就是int&&

情景三:ParamType既不是指针也不是引用

ParamType既不是指针也不是引用时,通过传值的方式处理。

template<typename T>
voidf(T param);                //以传值的方式处理param

无论传递什么,param都会成为它的一份拷贝一个完整的新对象。param成为一个新对象这一行为会影响T如何从expr中推导出结果。如果expr的类型是一个引用,忽略这个引用部分。如果忽略expr的引用性之后,expr是一个const,那就再忽略const。如果它是volatile,也忽略volatile

int x=27;                       //如之前一样
const int cx=x;                 //如之前一样
const int & rx=cx;              //如之前一样
f(x);                           //T和param的类型都是int
f(cx);                          //T和param的类型都是int
f(rx);                          //T和param的类型都是int

注意即使cxrx表示const值,param也不是constparam是一个完全独立于cxrx的对象——是cxrx的一个拷贝。具有常量性的cxrx不可修改并不代表param也是一样。这就是为什么expr的常量性在推导param类型时会被忽略:因为expr不可修改并不意味着它的拷贝也不能被修改。

只有在传值给形参时才会忽略const(volatile),对于reference-to-const和pointer-to-const形参来说,exprconst在推导时会被保留。

如果expr是一个const指针,指向const对象,expr通过传值传递给param

template<typename T>
voidf(T param);                //仍然以传值的方式处理param
const char* const ptr = ".."        //ptr是一个常量指针
f(ptr);                         //传递const char * const类型的实参

在这里,解引用符号(*)的右边的const表示ptr本身是一个constptr不能被修改为指向其它地址,也不能被设置为null(解引用符号左边的const表示ptr指向一个字符串,这个字符串是const,因此字符串不能被修改)。

ptr作为实参传给f,组成这个指针的每一比特都被拷贝进param

ptr自身的值会被传给形参,根据类型推导的第三条规则,ptr自身的常量性const将会被省略,所以paramconst char*,一个可变指针指向const字符串。在类型推导中,这个指针指向的数据的常量性const将会被保留,但是当拷贝ptr来创造一个新指针param时,ptr自身的常量性const将会被忽略。

数组实参

比如数组类型不同于指针类型,虽然它们两个有时候是可互换的。关于这个错觉最常见的例子是,在很多上下文中数组会退化为指向它的第一个元素的指针。

constchar name[] = "J. P. Briggs";     //name的类型是const char[13]
constchar * ptrToName = name;          //数组退化为指针

在这里const char*指针ptrToName会由name初始化,而name的类型为const char[13],这两种类型(const char*const char[13])是不一样的,但是由于数组退化为指针的规则,编译器允许这样的代码。

但要是一个数组传值给一个模板会怎样?会发生什么?

template<typename T>
void f(T param); //传值形参的模板
f(name);  //T和param会推导成什么类型?

有一个函数的形参是数组,是的,这样的语法是合法的,

void myFunc(int param[]);

但是数组声明会被视作指针声明,这意味着myFunc的声明和下面声明是等价的:

void myFunc(int* param);                //与上面相同的函数

数组与指针形参这样的等价是C语言的产物,C++又是建立在C语言的基础上,它让人产生了一种数组和指针是等价的的错觉。因为数组形参会视作指针形参,所以传值给模板的一个数组类型会被推导为一个指针类。在模板函数f的调用中,它的类型形参T会被推导为const char*

f(name);                        //name是一个数组,但是T被推导为const char*

虽然函数不能声明形参为真正的数组,但是可以接受指向数组的引用修改f为传引用:

template<typename T>
voidf(T& param);                       //传引用形参的模板

我们这样进行调用

f(name);                                //传数组给f

T被推导为了真正的数组!这个类型包括了数组的大小,在这个例子中T被推导为const char[13]f的形参(该数组的引用)的类型则为const char (&)[13]。这种语法看起来又臭又长,但是知道它将会让你在关心这些问题的人的提问中获得大神的称号。

有趣的是,可声明指向数组的引用的能力,使得我们可以创建一个模板函数来推导出数组的大小:

//在编译期间返回一个数组大小的常量值
constexpr std::size_t arraySize(T (&)[N]) noexcept    return N;                                 
}                                                      

在Item15提到将一个函数声明为constexpr使得结果在编译期间可用。这使得我们可以用一个花括号声明一个数组,然后第二个数组可以使用第一个数组的大小作为它的大小,就像这样:

int keyVals[] = { 1, 3, 7, 9, 11, 22, 35 };             //keyVals有七个元素int maint Vals=[arraySize(keyVals)];                     //mappedVals也有七个
std::array<int, arraySize(keyVals)> mappedVals;         //mappedVals的大小为7

至于arraySize被声明为noexcept,会使得编译器生成更好的代码,具体的细节请参见Item14。

函数实参

在C++中不只是数组会退化为指针,函数类型也会退化为一个函数指针,我们对于数组类型推导的全部讨论都可以应用到函数类型推导和退化为函数指针上来。结果是:

void someFunc(int, double);         //someFunc是一个函数,//类型是void(int, double)
template<typename T>
void f1(T param);                   //传值给f1template<typename T>
void f2(T & param);                 //传引用给f2
f1(someFunc);//param被推导为指向函数的指针,//类型是void(*)(int, double)
f2(someFunc); //param被推导为指向函数的引用,//类型是void(&)(int, double)

如果数组可以退化为指针,函数也会退化为指针。auto依赖于模板类型推导。然而,数组和函数退化为指针把这团水搅得更浑浊。有时你只需要编译器告诉你推导出的类型是什么。这种情况下,翻到item4,它会告诉你如何让编译器这么做。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/60702.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

柯桥生活英语口语学习“面坨了”英语怎么表达?

“面坨了”英语怎么表达&#xff1f; 要想搞清楚这个表达&#xff0c;首先&#xff0c;我们要搞明白“坨”是啥意思&#xff1f; 所谓“坨”就是指&#xff0c;面条在汤里泡太久&#xff0c;从而变涨&#xff0c;黏糊凝固在一起的状态。 有一个词汇&#xff0c;很适合用来表达这…

ZeroSSL HTTPS SSL证书ACMESSL申请3个月证书

目录 一、引言 二、准备工作 三、申请 SSL 证书 四、证书选型 五、ssl重要性 一、引言 目前免费 Lets Encrypt、ZeroSSL、BuyPass、Google Public CA SSL 证书&#xff0c;一般免费3-6个月。从申请难易程度分析&#xff0c;zerossl申请相对快速和简单&#xff0c;亲测速度非…

Java连接MySQL(测试build path功能)

Java连接MySQL&#xff08;测试build path功能&#xff09; 实验说明下载MySQL的驱动jar包连接测试的Java代码 实验说明 要测试该情况&#xff0c;需要先安装好MySQL的环境&#xff0c;其实也可以通过测试最后提示的输出来判断build path是否成功&#xff0c;因为如果不成功会直…

第四节-OSI-网络层

数据链路层&#xff1a;二层--MAC地址精确定位 Ethernet 2&#xff1a; 报头长度&#xff1a;18B 携带的参数&#xff1a;D MAC /S MAC/TYPE(标识上层协议)/FCS 802.3 报头长度&#xff1a;26B 携带的参数&#xff1a;D MAC/S MAC/LLC(标识上层协议)/SNAP&#xff08;标识…

labview实现功能性全局变量

在日常的项目中&#xff0c;笔者最长使用的就是全局变量&#xff0c;这样用起来不仅省心省力&#xff0c;而且传值也很方便&#xff0c;没有什么阻碍&#xff0c;想要传什么数据一根线拉过去就可以了。后面才知道如果一直使用全局变量会导致读写卡死的状态&#xff0c;而且还有…

网络安全之SQLMAP _DNS注入配置方法

网上针对sqlmap进行dns注入的相关文章太少&#xff0c;只是简单介绍了下–dns-domain参数&#xff0c;相关的实战文章要么就模糊或者一笔带过&#xff0c;。然后参考网上的方法重新整理了一遍&#xff0c;简单理解。 需要准备的东西&#xff0c;sqlmap、windows盲注一个、两个…

pycharm快速更换虚拟环境

目录 1. 选择Conda 虚拟环境2. 创建环境3. 直接选择现有虚拟环境 1. 选择Conda 虚拟环境 2. 创建环境 3. 直接选择现有虚拟环境

联想“喜新厌旧”

科技新知 原创作者丨萧维 编辑丨蕨影 十月份&#xff0c;联想很忙。 先是2024联想科技创新大会15日在美国华盛顿州西雅图举行&#xff0c;联想大秀了一下自己在人工智能领域的创新产品、技术和解决方案&#xff0c;英特尔、AMD、英伟达三巨头更同时为其站台&#xff1b;后是与…

[白月黑羽]关于仿写类postman功能软件题目的解答

原题&#xff1a; 答&#xff1a; python文件如下 from PySide6.QtWidgets import QApplication, QMessageBox,QTableWidgetItem,QHeaderView,QWidget,QTableWidget from PySide6.QtCore import QEvent,QObject from PySide6.QtUiTools import QUiLoader import time import …

零基础Java第十八期:图书管理系统

目录 一、package book 1.1. Book 1.2. BookList 二、package user 2.1. User 2.2. NormalUser与AdminiUser 三、Main 四、NormalUser与AdminiUser的菜单界面 五、package operation 5.1. 设计管理员菜单 六、业务逻辑 七、完整代码 今天博主来带大家实现一个…

系统架构师考试极限18天备考复盘(2024年11月)

前言 写下这篇复盘笔记的时候还没有出成绩。目前泽崽还是在读研究生&#xff0c;在经过 大概2周多个全日 的极限备考之后&#xff0c;于11月10日参加了软考的系统架构师考试&#xff08;高级&#xff09;。目前对于“基础知识-案例分析-论文”的估分预期大概是&#xff1a;55-…

Unity肢体控制(关节控制)

前面的基础搭建网上自己搜&#xff0c;我这个任务模型网上也有&#xff0c;可以去官网看看更多模型&#xff0c;这里只讲述有模型如何驱动肢体的操作方式 第一步&#xff1a;创建脚本 第二步&#xff1a;创建Rig Builder 建空容器 加部件&#xff08;Rig&#xff09;,加了之后…

二叉树遍历的非递归实现和复杂度分析

一&#xff0c;用栈实现二叉树先序遍历 1&#xff0c;原理 我用自己的口水话解释一下&#xff1a;准备一个栈&#xff0c;从根节点开始&#xff0c;先判断栈是否为空&#xff0c;如果否&#xff0c;就弹出一个元素&#xff0c;对弹出元素进行自定义处理&#xff0c;再将它的左…

redis序列化数据查询

可以看到是HashMap&#xff0c;那么是序列化的数据 那么我们来获得反序列化数据 import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.ObjectInputStream; import redis.clients.jedis.Jedis;public class RedisDeserializeDemo {public static…

球差控制操作数【ZEMAX操作数】

在光学设计中&#xff0c;对于球差的控制是必要的&#xff0c;那么在zemax中如何控制球差的大小&#xff0c;理解球差&#xff0c;以及使用相应操作数控制球差&#xff1b; 在这篇中主要写如何使用zemax操作数去控制或者消除球差&#xff0c;对球差进行简单的描述&#xff0c;之…

学习threejs,使用TWEEN插件实现动画

&#x1f468;‍⚕️ 主页&#xff1a; gis分享者 &#x1f468;‍⚕️ 感谢各位大佬 点赞&#x1f44d; 收藏⭐ 留言&#x1f4dd; 加关注✅! &#x1f468;‍⚕️ 收录于专栏&#xff1a;threejs gis工程师 文章目录 一、&#x1f340;前言1.1 ☘️THREE.PLYLoader PLY模型加…

前端 JS 实用操作总结

目录 1、重构解构 1、数组解构 2、对象解构 3、...展开 2、箭头函数 1、简写 2、this指向 3、没有arguments 4、普通函数this的指向 3、数组实用方法 1、map和filter 2、find 3、reduce 1、重构解构 1、数组解构 const arr ["唐僧", "孙悟空&quo…

从0开始学习--Day26--聚类算法

无监督学习(Unsupervised learning and introduction) 监督学习问题的样本 无监督学习样本 如图&#xff0c;可以看到两者的区别在于无监督学习的样本是没有标签的&#xff0c;换言之就是无监督学习不会赋予主观上的判断&#xff0c;需要算法自己去探寻区别&#xff0c;第二张…

矩阵数组转置

#include<stdio.h> int main() {int arr1[3][4];//三行四列变成四行三列int arr2[4][3];for(int i0;i<3;i)//三行{for(int j0;j<4;j)//四列{scanf("%d",&arr1[i][j]);//录入}}for(int i0;i<3;i)//转置{for(int j0;j<4;j){arr2[j][i]arr1[i][j]…

利用正则表达式批量修改文件名

首先&#xff0c; 我们需要稍微学习一下正则表达式的使用方式&#xff0c;可以看这里&#xff1a;Notepad正则表达式使用方法_notepad正则匹配-CSDN博客 经过初步学习之后&#xff0c;比较重要的内容我做如下转载&#xff1a; 元字符是正则表达式的基本构成单位&#xff0c;它们…