Effective Modern C++ 之 条款二:理解 auto 类型推导

如果你已经阅读了条款1关于模板相关的类型推导,你就已经知道了机会所有关于 auto 的类型推导,因为除了一个例外, auto 类型推导就是模板类型推导。但是它怎么就会是模板类型推导呢?模板类型推导涉及模板和函数以及参数,但是 auto 和上面的这些没有任何的关系。

这是对的,但是没有关系。模板类型推导和 auto 类型推导是有一个直接的映射。有一个书面上的从一种情况转换成另外一种情况的算法。
在条款1,模板类型推导是使用下面的通用模板函数来解释的:

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

在这里通常调用:

f(expr); // 使用一些表达式来当做调用f的参数

在调用 f 的地方,编译器使用 expr 来推导 T 和 ParamType 的类型。当一个变量被声明为 auto , auto 相当于模板中的 T ,而对变量做的相关的类型限定就像 ParamType 。这用代码说明比直接解释更加容易理解,所以看下面的这个例子:

auto x = 27;

这里,对 x 的类型定义就仅仅是 auto 本身。从另一方面,在这个声明中:

const auto cx = x;

类型被声明成 const auto ,在这儿:

const auto& rx = x;

类型被声明称 const auto& 。在这些例子中推导 x , cx , rx 的类型的时候,编译器处理每
个声明的时候就和处理对应的表达式初始化的模板:

template<typename T>               // 推导x的类型的
void func_for_x(T param);          // 概念上的模板

func_for_x(27);                    // 概念上的调用:
                                   // param的类型就是x的类型

template<typename T>
void func_for_cx(const T param);  // 推导cx的概念上的模板

func_for_cx(x);                   // 概念调用:param的推导类型就是cx的类型

template<typename T>
void func_for_rx(const T& param); // 推导rx概念上的模板

func_for_rx(x);                   // 概念调用:param的推导类型就是rx的类型

正如我所说,对 auto 的类型推导只存在一种情况的例外(这个后面就会讨论),其他的就和模板类型推导完全一样了。
条款1把模板类型推导划分成三部分,基于在通用的函数模板的 ParamType 的特性和 param 的类型声明。在一个用 auto 声明的变量上,类型声明代替了 ParamType 的作用,所以也有三种情况:

  • 情况1:类型声明是一个指针或者是一个引用,但不是一个通用的引用
  • 情况2:类型声明是一个通用引用
  • 情况3:类型声明既不是一个指针也不是一个引用

我们已经看了情况1和情况3的例子:

auto x = 27;        // 情况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是int并且是左值
                   // 所以uref2的类型是const int&

auto&& uref3 = 27; // 27是int并且是右值
                   // 所以uref3的类型是int&&

条款1讲解了在非引用类型声明里,数组和函数名称如何退化成指针。这在 auto 类型推导上面也是一样:

const char name[] = // name的类型是const char[13]
"R. N. Briggs";

auto arr1 = name;           // arr1的类型是const char*

auto& arr2 = name;          // arr2的类型是const char (&)[13]

void someFunc(int, double); // someFunc是一个函数,类型是
                            // void (*)(int, double)

auto& func2 = someFunc;     // func1的类型是
                           // void (&)(int, double)

正如你所见, auto 类型推导和模板类型推导工作很类似。它们就像一枚硬币的两面。
除了有一种情况是不一样的。我们从如果你想声明一个用27初始化的 int , C++98你有两种语法选择:

int x1 = 27;
int x2(27);

C++11,通过标准支持的统一初始化(使用花括号初始化——译者注),可以添加下面的代码:

int x3 = { 27 };
int x4{ 27 };

综上四种语法,都会生成一种结果:一个拥有27数值的 int 。
但是正如条款5所解释的,使用 auto 来声明变量比使用固定的类型更好,所以在上述的声明中把 int 换成 auto 更好。最直白的写法就如下面的代码:

auto x1 = 27;
auto x2(27);
auto x3 = {27};
auto x4{ 27 };

上面的所有声明都可以编译,但是他们和被替换的相对应的语句的意义并不一样。头两个的确是一样的,声明一个初始化值为27的 int 。然而后面两个,声明了一个类型为 std::intializer_list<int> 的变量,这个变量包含了一个单一的元素27!

auto x1 = 27;     // 类型时int,值是27

auto x2(27);      // 同上

auto x3 = { 27 }; // 类型是std::intializer_list<int>
                  // 值是{ 27 }

auto x4{ 27 };    // 同上

这和 auto 的一种特殊类型推导有关系。当使用一对花括号来初始化一个 auto 类型的变量的时候,推导的类型是 std::intializer_list 。如果这种类型无法被推导(比如在花括号中的变量拥有不同的类型),代码会编译错误。

auto x5 = { 1, 2, 3.0 }; // 错误! 不能讲T推导成
                         // std::intializer_list<T>

正如注释中所说的,在这种情况,类型推导会失败,但是认识到这里实际上是有两种类型推导是非常重要的。一种是 auto: x5 的类型被推导。因为 x5 的初始化是在花括号里面, x5 必须被推导成 std::intializer_list 。但是 std::intializer_list 是一个模板。实例
是对一些 T 实例化成 std::intializer_list<T> ,这就意味着 T 的类型必须被推导出来。类型推导就在第二种的推导的范围上失败了。在这个例子中,类型推导失败是因为在花括号里面的数值并不是单一类型的。

对待花括号初始化的行为是 auto 唯一和模板类型推导不一样的地方。当 auto 声明变量被使用一对花括号初始化,推导的类型是 std::intializer_list 的一个实例。但是如果相同的初始化递给相同的模板,类型推导会失败,代码不能编译。

auto x = { 11, 23, 9 }; // x的类型是
                        // std::initializer_list<int>

template<typename T>    // 和x的声明等价的
void f(T param);        // 模板

f({ 11, 23, 9 });       // 错误的!没办法推导T的类型

但是,如果你明确模板的 param 的类型是一个不知道 T 类型的 std::initializer_list<T> :

template<typename T>
void f(std::initializer_list<T> initList);

f({ 11, 23, 9 }); // T被推导成int,initList的
                  // 类型是std::initializer_list<int>

所以 auto 和模板类型推导的本质区别就是 auto 假设花括号初始化代表的是std::initializer_list,但是模板类型推导却不是。

你可能对为什么 auto 类型推导有一个对花括号初始化有一个特殊的规则而模板的类型推导却没有感兴趣。我自己也非常奇怪。可是我一直没有能够找到一个有力的解释。但是法则就是法则,这就意味着你必须记住如果使用 auto 声明一个变量并且使用花括号来初始化它,类型推导的就是 std::initializer_list 。你必须习惯这种花括号的初始化哲学——使用花括号里面的数值来初始化是理所当然的。在C++11编程里面的一个经典的错误就是误被声明成 std::initializer_list ,而其实你是想声明另外的一种类型。这个陷阱使得一些开发者仅仅在必要的时候才会在初始化数值周围加上花括号。(什么时候是必要的会在条款7里面讨论。)

对于C++11,这是一个完整的故事,但是对于C++14来说,故事还要继续。C++14允许 auto 表示推导的函数返回值(参看条款3),而且C++14的lambda可能会在参数声明里面使用 auto 。但是,这里面的使用是复用了模板的类型推导,而不是 auto 的类型推导。所以一个使用 auto 声明的返回值的函数,返回一个花括号初始化就无法编译。

auto createInitList()
{
    return { 1, 2, 3 }; // 编译错误:不能推导出{ 1, 2, 3 }的类型
}

在C++14的lambda里面,当 auto 用在参数类型声明的时候也是如此:

std::vector<int> v;
…

auto resetV =
[&v](const auto& newValue) { v = newValue; } // C++14

…
resetV({ 1, 2, 3 });  // 编译错误,不能推导出{ 1, 2, 3 }的类型

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值