现代C++学习笔记(5):auto与型别推导
Image (题图来自网络并做适当修改)
前一篇《现代C++学习笔记(4):auto的前世今生》介绍了 auto 关键字在 C++语言标准中的演化历史,这次结合《Effective Modern C++》的内容,继续看其相关用法。
1
前面说到,auto 对于简化代码,的确是一把利器。
最普遍存在的赋值表达式,总需要等号左右两边都是相同类型,或者至少是可以隐含转换成相同类型的,才能合法成立,也才可能被正确编译。因此,既然是相同类型,那么如果左边是一个新定义的变量,那它的类型,当然应该可以从等号右边的表达式推断得出。
代码简化的常规方法,是把需要写两遍或多遍的东西,合并到只写一遍。于是,把等号左边的变量类型定义,由具体的类型,改为 auto,就成了顺理成章的事。
auto i = 42; // int i = 42; auto l = 42l; // long l = 42l; auto ll = 42ll; // long long ll = 42ll; auto f = 4.2f; // float f = 4.2f; auto d = 4.2; // double d = 4.2; std::string s = “hello”; auto t = s + “, world”; // std::string t = … 很简单,很直观,也很方便,对吧?看起来就像弱类型语言的写法,但其背后却不失强类型语言的语法严格检查和运行效率。
2
由于赋值通常都是在进行“拷贝”,即把等号右侧的表达式,所计算出来的结果,拷贝一份,放入左侧的变量中。因此,auto 能够占位变量类型,但对于它所声明的变量,是否具备 const 属性,或是否为引用类型,却难以直接操控。
我们想要使用 auto 来定义一个 const 或引用时,还是需要显式写明,才能让编译器能够正确理解:
auto x = 42; // int x = 42; const auto cx = x; // const int cx = x; const auto& rx = x; // const int& rx = x; 如上,x 和 cx 是不同的变量,它们的地址不同(可以通过 & 获取地址,并将其打印出来,进行验证)。而 cx 带了 const 属性,因此不能在后面的代码中进行修改(其实这仅仅是编译器做的限定,如果你暴力地获取指针,在二进制代码级别,还是可以修改该变量的)。rx 是个引用,它其实是 x 的一个别名,如果取地址,x 与 rx 将得到相同的地址,同时,又由于它带有 const 属性,因此后续代码也同样不能修改 rx 的值(但原 x 可以被修改)。
3
对于上面的这三行变量定义(x、cx 和 rx),在“条款2:理解auto型别推导”中,是将其与模板推导放到一起来介绍的。
这有点不容易理解,于是书中给出了如下的伪代码,一下子就把 auto 变量定义式,与先前提到过的“型别推导”联系到一起了:
template
template
template
前面《现代C++学习笔记(2):型别推导基础》提到过,模板函数的参数型别推导,存在三种情况。而这里进行 auto 变量定义时,与之类似,也存在完全一样的三种情况:
情况1:型别修饰词是指针或引用,但不是万能引用
情况2:型别修饰词是万能引用
情况3:型别修饰词即非指针也非引用
情况1 和情况 3,在前面的例子中已经展示过:
auto x = 42; // 情况3(x即非指针也非引用) const auto cx = x; // 情况3(cx即非指针也非引用) const auto& rx = x; // 情况1(rx是个引用,但非万能引用) 情况2 的运行也与预期一致:
auto&& uref1 = x; // x 的型别是 int,且是左值, // 所以 uref1 的型别是 int& auto&& uref2 = cx; // cx 的型别是 const int,且是左值 // 所以 uref2 的型别是 const int& auto&& uref3 = rx; // rx 的型别是 const int&,且是左值 // 所以 uref3 的型别是 const int& auto&& uref4 = 42; // 42 的型别是 int,且是右值 // 所以 uref4 的型别是 int&& 4
看到上面,我相信大家一定都会跟我一样,完全认同了 auto 变量定义,的确就跟模板函数的型别推导,本质上是完全相同的。然而,就在这个时候,Scott Meyers 却说有一种情况例外,真是跌宕起伏,吊足胃口。
要解释这种情况,需要回到类的实例初始化语法来。这是一个新标准中的有意思的特性引入,它涉及到如何兼顾语法的一致优雅,同时又尽量不破坏旧代码的综合考虑。限于篇幅,我打算等后面单独用一篇来展开。
这里就暂时用书中提及的例子,简单带过:
首先是C++98标准提供的两种初始化变量的语法:
int x1 = 42; int x2(42); 然后是C++11新增的两种初始化变量的语法:
int x3 = { 42 }; int x4{42}; 上面四行代码,忽略变量名的差异,它们彼此之间其实是完全等价的。
然而,如果简单暴力地把 int 换成 auto,结果却并不与预期相同:
auto x1 = 42; // 型别是int,值是42
auto x2(42); // 同上
auto x3 = { 42 }; // 型别是std::initializer_list
5
auto用法中,这个突如其来的混乱,看起来更像是一个语法设计上的缺陷。按照书中的说法,“这个缺陷至今尚未得到解决”。
正如 Scott Meyers 在书中也声称:“我自己也奇怪。实话实说,对此我找不到一个有说服力的解释。但规则就是规则……”
所以,不要期待一个语法标准总是自洽、圆满和完美的。遇到特殊情况,记住并遵守就是。
6
不过,最后,我还是做了一次实验:
Image
其编译和运行结果如下:
Image
可见,与书中表达的不同,仅有第三种写法是存在混乱的(生成了初始化列表类型),第四种写法并未出现混乱(仍然是整数类型)。也许未来C++标准的发展,还真有可能最终解决这些混乱呢。
注:本文首发表于“不靠谱颜论”公众号,并同步至本站。