颜林林的个人网站

现代C++学习笔记(5):auto与型别推导

2022-01-30 00:59

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 void func_for_x(T param); func_for_x(42);

template void func_for_cx(const T param); func_for_cx(x);

template void func_for_rx(const T& param); func_for_rx(x); auto 与“型别推导”,原来就是同一回事嘛。

前面《现代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,值是{42} auto x4{42}; // 同上 其中 x3 与 x4 得到了完全出乎预料的结果,它们不再是 int 型别,而是成为了一个奇怪的“初始化列表”类型(std::initializer_list)。

5

auto用法中,这个突如其来的混乱,看起来更像是一个语法设计上的缺陷。按照书中的说法,“这个缺陷至今尚未得到解决”。

正如 Scott Meyers 在书中也声称:“我自己也奇怪。实话实说,对此我找不到一个有说服力的解释。但规则就是规则……”

所以,不要期待一个语法标准总是自洽、圆满和完美的。遇到特殊情况,记住并遵守就是。

6

不过,最后,我还是做了一次实验:

Image

其编译和运行结果如下:

Image

可见,与书中表达的不同,仅有第三种写法是存在混乱的(生成了初始化列表类型),第四种写法并未出现混乱(仍然是整数类型)。也许未来C++标准的发展,还真有可能最终解决这些混乱呢。

--- END ---

注:本文首发表于“不靠谱颜论”公众号,并同步至本站。

相关文章