c++避坑指南(七)

auto 推导策略

C++11 提供了auto来自动推导类型,很大程度上提升了代码的直观性,例如:

std::unordered_map<std::string, std::vector<int>> data_map; // 不用auto std::unordered_map<std::string, std::vector<int>>::iterator iter = data_map.begin(); // 使用auto推导 auto iter = data_map.begin();

但 auto 的推导仍然引入了不少奇怪的问题。首先,auto关键字仅仅是用来代替“类型符”的,它并没有改变“C++类型说明符具有多重意义”这件事,在前面“类型说明符”的章节我曾介绍过,C++中,类型说明符除了表示“类型”以外,还承担了“定义动作”的任务,auto可以视为一种带有类型推导的类型说明符,其本质仍然是类型说明符,所以,它同样承担了定义动作的任务,例如:

auto a = 5; // auto承担了“定义变量”的任务

auto却不可以和[]组合定义数组,比如:

auto arr[] = {1, 2, 3}; // ERR

在定义函数上,更加有趣,在 C++14 以前,并不支持用auto推导函数返回值类型,但是却支持返回值后置语法,所以在这种场景下,auto仅仅是一个占位符而已,它既不表示类型也不表示定义动作,仅仅就是为了结构完整占位而已:

auto func() -> int; // () -> int表示定义函数,int表示函数返回值类型

到了 C++14 才支持了返回值类型自动推导,但并不支持自动生成多种类型的返回值:

auto func(int cmd) {   if (cmd > 0) {     return 5; // 用5推导返回值为int   }   return std::string("123"); // ERR,返回值已经推导为int了,不能多类型返回 }

auto 的语义

同样还是出自这句话“auto 是用来代替类型说明符的”,因此auto在语义上也更加倾向于“用它代替类型说明符”这种行为,尤其是它和引用、指针类型结合时,这种特性更加明显:

int a = 5; const int k = 9; int &r = a; auto b = a; // auto->int auto c = 4; // auto->int auto d = k; // auto->int auto e = r; // auto->int

我们看到,无论用普通变量、只读变量、引用、常量去初始化 auto 变量时,auto都只会推导其类型,而不会带有左右性、只读性这些内容。

所以,auto的类型推导,并不是“推导某个表达式的类型”,而是“推导当前位置合适的类型”,或者可以理解为“这里最简单可以是什么类型”。比如说上面auto c = 4这里,auto可以推导为int,int &&,const int,const int &,const int &&,而auto选择的是里面最简单的那一种。

auto还可以跟指针符、引用符结合,而这种时候它还是满足上面“最简单”的这种原则,并且此时指的是“auto本身最简单”,举例来说:

int a = 5; auto p1 = &a; // auto->int * auto *p2 = &a; // auto->int auto &r1 = a; // auto->int auto *p3 = &p2; // auto->int * auto p4 = &p2; // auto-> int **

p1p2都是指针,但auto都是用最简原则来推导的,p2这里因为我们已经显式写了一个*了,所以auto只会推导出int,因此p2最终类型仍然是int *而不会变成int **。同样的道理在p3p4上也成立。

在一些将“类型”和“动作”语义分离的语言中,就完全不会有 auto 的这种困扰,它们可以用“省略类型符”来表示“自动类型推导”的语义,而起“定义”语义的关键字得以保留而不受影响,例如在 swift 中:

var a = 5 // Int let b = 5.6 // 只读Double let c: Double = 8 // 显式指定类型

在 Go 中也是类似的:

var a = 2.5 // var表示“定义变量”动作,自动推导a的类型为float64 b := 5 // 自动推导类型为int,:=符号表示了“定义动作”语义 const c = 7 // const表示“定义只读变量”动作,自动推导c类型为int var d float32 = 9 // 显式指定类型

auto 引用

在前面“引用折叠”的章节曾经提到过auto &&的推导原则,有可能会推导出左值引用来,所以auto &&并不是要“定义一个右值引用”,而是“定义一个保持左右性的引用”,也就是说,绑定一个左值时会推导出左值引用,绑定一个右值时会推导出右值引用。

int a = 5; int &r1 = a; int &&r2 = 4; auto &&y1 = a; // int & auto &&y2 = r1; // int & auto &&y3 = r2; // int &(注意右值引用本身是左值) auto &&y4 = 3; // int && auto &&y5 = std::move(r1); // int &&

更详细的内容可以参考前面“引用折叠”的章节。

C 语言曾经的 auto

我相信大家现在看到auto都第一印象是 C++当中的“自动类型推导”,但其实auto并不是 C++11 引入的新关键在,在原始 C 语言中就有这一关键字的。

在原始 C 中,auto表示“自动变量位置”,与之对应的是register。在之前“const 引用”章节中笔者曾经提到,“变量就是内存变量”,但其实在原始 C 中,除了内存变量以外,还有一种变量叫做“寄存器变量”,也就是直接将这个数据放到 CPU 的寄存器中。也就是说,编译器可以控制这个变量的位置,如果更加需要读写速度,那么放到寄存器中更合适,因此auto表示让编译器自动决定放内存中,还是放寄存器中。而register修饰的则表示人工指定放在寄存器中。至于没有关键字修饰的,则表示希望放到内存中。

int a; // 内存变量 register int b; // 寄存器变量 auto int c; // 由编译器自动决定放在哪里

需要注意的是,寄存器变量不能取址。这个很好理解,因为只有内存才有地址(地址本来指的就是内存地址),寄存器是没有的。因此,auto修饰的变量如果被取址了,那么一定会放在内存中:

auto int a; // 有可能放在内存中,也有可能放在寄存器中 auto int b; int *p = &b; // 这里b被取址了,因此b一定只能放在内存中 register int c; int *p2 = &c; // ERR,对寄存器变量取址,会报错

然而在 C++中,几乎不会人工来控制变量的存放位置了,毕竟 C++更加上层一些,这样超底层的语法就被摒弃了(C++11 取消了register关键字,而auto关键字也失去其本意,变为了“自动类型推导”的占位符)。而关于变量的存储位置则是全权交给了编译器,也就是说我们可以理解为,在 C++11 以后,所有的变量都是自动变量,存储位置由编译器决定。

原创文章受到原创版权保护。转载请注明出处:https://www.ccppcoding.com/archives/1260

转载文章受原作者版权保护。转载请注明原作者出处!

(0)
上一篇 2022年11月2日 下午12:18
下一篇 2022年11月2日 下午12:20

相关推荐