首页 > 其他分享 >06 | auto/decltype:为什么要有自动类型推导?

06 | auto/decltype:为什么要有自动类型推导?

时间:2024-04-03 12:01:17浏览次数:27  
标签:06 推导 int auto C++ 类型 decltype

我们从宏观的层面上重新认识了 C++,从今天开始,我们将进入一个新的“语言特性”单元,“下沉”到微观的层面去观察 C++,一起去见一些老朋友、新面孔,比如 const、exception、lambda。

这次要说的,就是 C++11 里引入的一个很重要的语言特性:自动类型推导。

自动类型推导

如果你有过一些 C++ 的编程经验,了解过 C++11,那就一定听说过“自动类型推导”(auto type deduction)。

它其实是一个非常“老”的特性,C++ 之父 Bjarne Stroustrup(B·S ) 早在 C++ 诞生之初就设计并实现了它,但因为与早期 C 语言的语义有冲突,所以被“雪藏”了近三十年。直到 C99 消除了兼容性问题,C++11 才让它再度登场亮相。

那为什么要重新引入这个“老特性”呢?为什么非要有“自动类型推导”呢?

我觉得,你可以先从字面上去理解,把这个词分解成三个部分:“自动”“类型”和“推导”。

“自动”就是让计算机去做,而不是人去做,相对的是“手动”。

“类型”指的是操作目标,出来的是编译阶段的类型,而不是数值。

“推导”就是演算、运算,把隐含的值给算出来。

好,我们来看一看“自动类型推导”之外的其他几种排列组合,通过对比的方式来帮你理解它。

像计算“a = 1 + 1”,你可以在写代码的时候直接填上 2,这就是“手动数值推导”。你也可以“偷懒”,只写上表达式,让电脑在运行时自己算,这就是“自动数值推导”。

“数值推导”对于人和计算机来说都不算什么难事,所以手动和自动的区别不大,只有快慢的差异。但“类型推导”就不同了。

因为 C++ 是一种静态强类型的语言,任何变量都要有一个确定的类型,否则就不能用。在“自动类型推导”出现之前,我们写代码时只能“手动推导”,也就是说,在声明变量的时候,必须要明确地给出类型。

这在变量类型简单的时候还好说,比如 int、double,但在泛型编程的时候,麻烦就来了。因为泛型编程里会有很多模板参数,有的类型还有内部子类型,一下子就把 C++ 原本简洁的类型体系给搞复杂了,这就迫使我们去和编译器“斗智斗勇”,只有写对了类型,编译器才会“放行”(编译通过)。

int       i = 0;            // 整数变量,类型很容易知道
double    x = 1.0;          // 浮点数变量,类型很容易知道

std::string str = "hello";  // 字符串变量,有了名字空间,麻烦了一点

std::map<int, std::string> m = // 关联数组,名字空间加模板参数,很麻烦
        {{1,"a"}, {2,"b"}};    // 使用初始化列表的形式

std::map<int, std::string>::const_iterator // 内部子类型,超级麻烦
iter = m.begin();

??? = bind1st(std::less<int>(), 2);  // 根本写不出来

虽然你可以用 typedef 或者 using 来简化类型名,部分减轻打字的负担,但关键的“手动推导”问题还是没有得到解决,还是要去翻看类型定义,找到正确的声明。这时,C++ 的静态强类型的优势反而成为了劣势,阻碍了程序员的工作,降低了开发效率。

其实编译器是知道(而且也必须知道)这些类型的,但它却没有办法直接告诉你,这就很尴尬了。一边是急切地想知道答案,而另一边却只给判个对错,至于怎么错了、什么是正确答案,“打死了也不说”。

但有了“自动类型推导”,问题就迎刃而解了。这就像是在编译器紧闭的大门上开了道小口子,你跟它说一声,它就递过来张小纸条,具体是什么不重要,重要的是里面存了我们想要的类型。

这个“小口子”就是关键字 auto,在代码里的作用像是个“占位符”(placeholder)。写上它,你就可以让编译器去自动“填上”正确的类型,既省力又省心。

auto  i = 0;          // 自动推导为int类型
auto  x = 1.0;        // 自动推导为double类型

auto  str = "hello";  // 自动推导为const char [6]类型

std::map<int, std::string> m = {{1,"a"}, {2,"b"}};  // 自动推导不出来

auto  iter = m.begin();  // 自动推导为map内部的迭代器类型

auto  f = bind1st(std::less<int>(), 2);  // 自动推导出类型,具体是啥不知道

不过需要注意的是,因为 C++ 太复杂,“自动类型推导”有时候可能失效,给不出你想要的结果。比如,在上面的这段代码里,就把字符串的类型推导成了“const char [6]”而不是“std::string”。而有的时候,编译器也理解不了代码的意思,推导不出恰当的类型,还得你自己“亲力亲为”。

在这个示例里,你还可以直观感觉到 auto 让代码干净整齐了很多,不用去写那些复杂的模板参数了。但如果你把“自动类型推导”理解为仅仅是简化代码、少打几个字,那就实在是浪费了 C++ 标准委员会的一番苦心。

除了简化代码,auto 还避免了对类型的“硬编码”,也就是说变量类型不是“写死”的,而是能够“自动”适应表达式的类型。比如,你把 map 改为 unordered_map,那么后面的代码都不用动。这个效果和类型别名(第 5 讲)有点像,但你不需要写出 typedef 或者 using,全由 auto“代劳”。

另外,你还应该认识到,“自动类型推导”实际上和“attribute”一样(第 4 讲),是编译阶段的特殊指令,指示编译器去计算类型。所以,它在泛型编程和模板元编程里还有更多的用处,后面我会陆续讲到。

认识 auto

刚才说了,auto 有时候会不如你设想的那样工作,因此在使用的时候,有一些需要特别注意的地方,下面就给你捋一捋。

首先,你要知道,auto 的“自动推导”能力只能用在“初始化”的场合。

具体来说,就是赋值初始化或者花括号初始化(初始化列表、Initializer list),变量右边必须要有一个表达式(简单、复杂都可以)。这样你才能在左边放上 auto,编译器才能找到表达式,帮你自动计算类型。

如果不是初始化的形式,只是“纯”变量声明,那就无法使用 auto。因为这个时候没有表达式可以让 auto 去推导。

auto x = 0L;    // 自动推导为long
auto y = &x;    // 自动推导为long*
auto z {&x};    // 自动推导为long* 

auto err;       // 错误,没有赋值表达式,不知道是什么类型

这里还有一个特殊情况,在类成员变量初始化的时候(第 5 讲),目前的 C++ 标准不允许使用 auto 推导类型(但我个人觉得其实没有必要,也许以后会放开吧)。所以,在类里你还是要老老实实地去“手动推导类型”。

class X final
{
    auto a = 10;  // 错误,类里不能使用auto推导类型
};

知道了应用场合,你还需要了解 auto 的推导规则,保证它能够按照你的意思去工作。虽然标准里规定得很复杂、很细致,但我总结出了两条简单的规则,基本上够用了:

auto 总是推导出“值类型”,绝不会是“引用”;

auto 可以附加上 const、volatile、*、& 这样的类型修饰符,得到新的类型。

下面举几个例子,你一看就能明白:

auto        x = 10L;    // auto推导为long,x是long

auto&       x1 = x;      // auto推导为long,x1是long&
auto*       x2 = &x;    // auto推导为long,x2是long*
const auto& x3 = x;        // auto推导为long,x3是const long&
auto        x4 = &x3;    // auto推导为const long*,x4是const long*

认识 decltype

前面我都在说 auto,其实,C++ 的“自动类型推导”还有另外一个关键字:decltype

刚才你也看到了,auto 只能用于“初始化”,而这种“向编译器索取类型”的能力非常有价值,把它限制在这么小的场合,实在是有点“屈才”了。

“自动类型推导”要求必须从表达式推导,那在没有表达式的时候,该怎么办呢?

其实解决思路也很简单,就是“自己动手,丰衣足食”,自己带上表达式,这样就走到哪里都不怕了。

decltype 的形式很像函数,后面的圆括号里就是可用于计算类型的表达式(和 sizeof 有点类似),其他方面就和 auto 一样了,也能加上 const、*、& 来修饰。

但因为它已经自带表达式,所以不需要变量后面再有表达式,也就是说可以直接声明变量。

int x = 0;          // 整型变量

decltype(x)     x1;      // 推导为int,x1是int
decltype(x)&    x2 = x;    // 推导为int,x2是int&,引用必须赋值
decltype(x)*    x3;      // 推导为int,x3是int*
decltype(&x)    x4;      // 推导为int*,x4是int*
decltype(&x)*   x5;      // 推导为int*,x5是int**
decltype(x2)    x6 = x2;  // 推导为int&,x6是int&,引用必须赋值

把 decltype 和 auto 比较一下,简单来看,好像就是把表达式改到了左边而已,但实际上,在推导规则上,它们有一点细微且重要的区别:

decltype 不仅能够推导出值类型,还能够推导出引用类型,也就是表达式的“原始类型”。

在示例代码中,我们可以看到,除了加上 * 和 & 修饰,decltype 还可以直接从一个引用类型的变量推导出引用类型,而 auto 就会把引用去掉,推导出值类型。

所以,你完全可以把 decltype 看成是一个真正的类型名,用在变量声明、函数参数 / 返回值、模板参数等任何类型能出现的地方,只不过这个类型是在编译阶段通过表达式“计算”得到的。

如果不信的话,你可以用 using 类型别名来试一试。

using int_ptr = decltype(&x);    // int *
using int_ref = decltype(x)&;    // int &

既然 decltype 类型推导更精确,那是不是可以替代 auto 了呢?

实际上,它也有个缺点,就是写起来略麻烦,特别在用于初始化的时候,表达式要重复两次(左边的类型计算,右边的初始化),把简化代码的优势完全给抵消了。

所以,C++14 就又增加了一个“decltype(auto)”的形式,既可以精确推导类型,又能像 auto 一样方便使用。

int x = 0;            // 整型变量

decltype(auto)     x1 = (x);  // 推导为int&,因为(expr)是引用类型
decltype(auto)     x2 = &x;   // 推导为int*
decltype(auto)     x3 = x1;   // 推导为int&

使用 auto/decltype

现在,我已经讲完了“自动类型推导”的两个关键字:auto 和 decltype,那么,该怎么用好它们呢?

我觉得,因为 auto 写法简单,推导规则也比较好理解,所以,在变量声明时应该尽量多用 auto。前面已经举了不少例子,这里就不再重复了。

auto 还有一个“最佳实践”,就是“range-based for”,不需要关心容器元素类型、迭代器返回值和首末位置,就能非常轻松地完成遍历操作。不过,为了保证效率,最好使用“const auto&”或者“auto&”。

 vector<int> v = {2,3,5,7,11};  // vector顺序容器

 for(const auto& i : v) {      // 常引用方式访问元素,避免拷贝代价
     cout << i << ",";          // 常引用不会改变元素的值
 }

 for(auto& i : v) {          // 引用方式访问元素
     i++;                      // 可以改变元素的值
     cout << i << ",";
 }

在 C++14 里,auto 还新增了一个应用场合,就是能够推导函数返回值,这样在写复杂函数的时候,比如返回一个 pair、容器或者迭代器,就会很省事。

auto get_a_set()              // auto作为函数返回值的占位符
{
    std::set<int> s = {1,2,3};
    return s;
}

再来看 decltype 怎么用最合适。

它是 auto 的高级形式,更侧重于编译阶段的类型计算,所以常用在泛型编程里,获取各种类型,配合 typedef 或者 using 会更加方便。当你感觉“这里我需要一个特殊类型”的时候,选它就对了。

比如说,定义函数指针在 C++ 里一直是个比较头疼的问题,因为传统的写法实在是太怪异了。但现在就简单了,你只要手里有一个函数,就可以用 decltype 很容易得到指针类型。

// UNIX信号函数的原型,看着就让人晕,你能手写出函数指针吗?
void (*signal(int signo, void (*func)(int)))(int)

// 使用decltype可以轻松得到函数指针类型
using sig_func_ptr_t = decltype(&signal) ;

在定义类的时候,因为 auto 被禁用了,所以这也是 decltype 可以“显身手”的地方。它可以搭配别名任意定义类型,再应用到成员变量、成员函数上,变通地实现 auto 的功能。

class DemoClass final
{
public:
    using set_type      = std::set<int>;  // 集合类型别名
private:
    set_type      m_set;                   // 使用别名定义成员变量

    // 使用decltype计算表达式的类型,定义别名
    using iter_type = decltype(m_set.begin());

    iter_type     m_pos;                   // 类型别名定义成员变量
};

小结

好了,今天介绍了 C++ 里的“自动类型推导”,简单小结一下今天的内容。

1. “自动类型推导”是给编译器下的指令,让编译器去计算表达式的类型,然后返回给程序员。

2. auto 用于初始化时的类型推导,总是“值类型”,也可以加上修饰符产生新类型。它的规则比较好理解,用法也简单,应该积极使用。

3. decltype 使用类似函数调用的形式计算表达式的类型,能够用在任意场合,因为它就是一个编译阶段的类型。

4. decltype 能够推导出表达式的精确类型,但写起来比较麻烦,在初始化时可以采用 decltype(auto) 的简化形式。

5. 因为 auto 和 decltype 不是“硬编码”的类型,所以用好它们可以让代码更清晰,减少后期维护的成本。

课下作业

1. auto 和 decltype 虽然很方便,但用多了也确实会“隐藏”真正的类型,增加阅读时的理解难度,你觉得这算是缺点吗?是否有办法克服或者缓解?

2. 说一下你对 auto 和 decltype 的认识。你认为,两者有哪些区别呢?(推导规则、应用场合等)



C++11引入了自动类型推导(auto type deduction)和decltype这两个重要语言特性,它们允许编译器根据初始化表达式自动推导变量的类型,从而简化代码、提高开发效率。auto用于初始化时的类型推导,总是“值类型”,也可以加上修饰符产生新类型,而decltype不仅能够推导出值类型,还能够推导出引用类型,也就是表达式的“原始类型”。在使用auto时,需要遵循其推导规则,以确保其按照预期工作。而decltype则更侧重于编译阶段的类型计算,常用在泛型编程里,获取各种类型,配合typedef或者using会更加方便。总的来说,自动类型推导不仅简化了代码,还避免了对类型的硬编码,提高了代码的灵活性和可维护性。auto和decltype虽然很方便,但用多了也确实会“隐藏”真正的类型,增加阅读时的理解难度,但可以通过一些方法来克服或者缓解这一问题。auto还有一个“最佳实践”,就是“range-based for”,不需要关心容器元素类型、迭代器返回值和首末位置,就能非常轻松地完成遍历操作。decltype还可以用于定义类的成员变量、成员函数,变通地实现auto的功能。通过使用好auto和decltype,可以让代码更清晰,减少后期维护的成本。 

标签:06,推导,int,auto,C++,类型,decltype
From: https://blog.csdn.net/qq_37756660/article/details/137234096

相关文章

  • Day 06 Linux的进程管理
    相关定义程序二进制文件,静态/usr/sbin/httpd,/usr/sbin/sshd,程序占用磁盘空间程序的两种状态:running和dead进程是程序运行的过程,动态,有生命周期的,可以产生和消亡的(进程是已启动的可执行程序的运行实例,实例即运行可执行程序),进程占用CPU和内存mem。父进程程序运行时产......
  • AGC066 题解
    题解:AT_agc066_a[AGC066A]AdjacentDifference笑点解析:没有必要将总成本最小化。我们将格子间隔的黑白染色(显然有两种染色方法),对于黑点我们要求它是奇数倍\(d\),对于白点我们要求它是偶数倍\(d\),这样一定满足相邻格子相差至少\(d\)。因为两种染色方法的代价和为\(dN^2\),......
  • Python控制安卓模拟器——uiautomator2模块
    Python控制安卓模拟器——uiautomator2模块目录Python控制安卓模拟器——uiautomator2模块介绍【1】安装python【2】安装adb1]下载[adb:[2]配置环境变量【3】安装uiautomator2【4】连接设备(安卓模拟器)【5】u2指令控制设备常用指令【6】安装weditor【7】元素操作元......
  • Java方法06:递归讲解
    递归1.A方法调用B方法,我们很容易理解!2.递归就是:A方法调用A方法!就是自己调用自己3.利用递归可以用简单的程序来解决一些复杂的问题。它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解,递归策略只需少量的程序就可描述出解题过程所需要......
  • set autotrace on
    在SQL*Plus中,你可以通过设置autotrace选项来在执行SQL命令的同时,自动的获得语句的执行计划和附加的统计信息。AUTOTRACE是一个很出色的OracleSQL语句的诊断工具,与Explainplan不同的是这条SQL是实际执行了的,同时AUTOTRACE使用起来也极为方便。一、启用Autotrace功能。任何以SQL*......
  • autohotkey的使用心得, 和最近写的点击屏幕三次自动算夹角的工具.
    https://github.com/zhangbo2008/arc_tools_by_click_mouse_three_times autohotkey如何debug: vscode里面安装上,autohotkeypuls即可. 然后直接运行我们写的1.ahk,他就会自动找autohotkey.exe的程序来debug了. autohotkey的赋值写法: 传统方法 使用百分号括住变量名......
  • kerberos-MS14-068(kerberos域用户提权)
    微软官方在2014年11月18日发布了一个紧急补丁,Windows全版本服务器系统受到影响,包括WindowsServer2003,WindowsServer2008,WindowsServer2008R2,WindowsServer2012和WindowsServer2012R2,修复了MicrosoftWindowsKerberosKDC(CVE-2014-6324),该漏洞可导致活动目录整体权......
  • AutoCAD2024中标注的字体和箭头都很小看不清怎么办?
    在使用AutoCAD绘图的过程中,偶尔会出现标注字体和箭头很小,看不清楚的情况,如下,这种情况一般会出现在我们按照1:1绘图画大型尺寸图纸时,这主要是因为CAD默认的标注样式下,字体和箭头大小默认是2.5,而当图形尺寸较大时,标注文字和箭头相对就太小了,必须放大后才可以看到,下面给大家分享一下......
  • uniapp_06_全局消息提醒(App端)
    uniapp全局消息提醒(App端)前言最近在项目中需要用到全局消息提醒,才发现App.vue文件虽然是页面入口文件但是App.vue文件本身不是页面。之后试了创建一个全局组件挂载在vue原型上,但是发现在h5中没有问题,但是在app和小程序中由于不存在document导致报错。最后想到了3个解......
  • 30 天精通 RxJS (06): 建立 Observable(二)
    CreationOperatorObservable有许多创建实例的方法,称为creationoperator。下面我们列出RxJS常用的creationoperatorcreateoffromfromEventfromPromiseneveremptythrowintervaltimerof还记得我们昨天用create来建立一个同步处理的observable吗?varsou......