理解 auto 类型推断
上一篇帖子中讲述了模板类型推断, 我们知道 auto 的实现原理是基于模板类型推断的, 回顾一下模板类型推断:
- template <typename T>
- void f(ParamType param);
使用下面的函数调用:
f(expr);
我们看到模板类型推断过程涉及到了模板 template, 函数 f 以及参数 (包括模板参数和函数参数), 调用 f 的时候, 编译器会推断 T 和 ParamType 的类型. auto 的实现和这三个部分是有着对应关系的. 当使用 auto 声明一个变量, auto 关键字扮演的是模板类型推断中 T 的角色, 而类型说明符扮演的是 ParamType 的角色. 看下面的例子:
- auto x = 27; // 类型说明符就是 auto 自己
- const auto cx =x; // 类型说明符为 const auto
- const auto& rx =x;// 类型说明符为 const auto&
编译器使用 auto 对上面的类型进行推断就如同使用了下面的模板类型推断:
- template<typename T>
- void func_for_x(T param); //ParamType 即非引用也非指针
- func_for_x(27); // 推断 x 的类型, T 为 int ,ParamType 为 int
- template<typename T>
- void func_for_cx(const T param); //ParamType 即非引用也非指针
- func_for_cx(x); // 用于推断 cx 的类型, T 为 int,ParamType 为 const int
- template<typename T>
- void func_for_rx(const T& param);//ParamType 为引用
- func_for_rx(x); // 用于推断 rx 的类型
继续回顾上一篇帖子的内容, 基于 ParamType 的三种形式, 模板类型推断也对应着三种不同情况. 而 auto 的类型说明符扮演的是 ParamType, 因此使用 auto 进行变量声明, 也会有三种情况:
类型说明符是指针或者引用类型, 但不是 universal reference
类型说明符是 universal reference.
类型说明符即非指针也非引用.
上面举的例子是第一种和第三种情况:
- auto x = 27; //case 3 x 类型被推断为 int
- const auto cx = x; //case 3 cx 被推断为 const int
- const auto &rx = x; //case 1 rx 被推断为 const int &
举一个情况 2 的例子:
- auto&& uref1 = x; //x 为左值, uref1 被推断为左值引用
- auto&& uref2 = cx; // cx const int 左值, uref2 被推断为 const int &
- auto&& uref3 = 27; // 27 为 int 右值, uref3 被推断为 int &&
上篇帖子介绍了对于模板中的非引用 ParamType, 传入函数或者数组实参的时候会退化为指针的情况 (而使用引用 ParamType 的时候, 数组实参会被推断为指向数组的引用),auto 类型推断也会如此:
- const char name[] = "R. N. Briggs";
- auto arr1 = name; // arr1 的类型为 const char*
- auto& arr2 = name; // arr2 的类型为 const char (&)[13]
- void someFunc(int, double);
- auto func1 = someFunc; // func1 的 类型为 void (*)(int, double)
- auto& func2 = someFunc; // func2 的类型为 void (&)(int, double)
上面介绍的都是 auto 和模板类型推断使用原理相同的部分, 下面说的不一样的.
C++98 中初始化一个 Int 有两种方式:
- int x1=27;
- int x1(27);
在 C++11 中, 支持统一初始化 (uniform initialization):
- int x3 = {
- 27
- };
- int x3{
- 27
- };
四种语法形式的结果只有一个, 初始化一个 Int 值为 27. 这里我们将都使用 auto 进行初始化:
- auto x1 = 27;
- auto x2(27);
- auto x3 = {27};
- auto x4{27};
上面的四句话都能编译通过, 但并没有和原来的四种形式意义完全一致. 前面两个是一样的, 后面两句话声明的变量类型是 std::initializer_list, 其中包含了单个元素, 值为 27.
- auto x1 = 27; //x1 为 int, 值为 27
- auto x2(27);// 同上
- auto x3 = {27};//x3 为 std::initializer_list<int>, 值为 {27}
- auto x4{27}; // 同上
这里就用到了一个对于 auto 的特殊类型推断规则: 当用大括号括起来的值对 auto 变量进行初始化的时候 (叫做统一初始化式), 变量类型会被推断为 std::initializer_list. 如果不能够推断成此类型 (比如, 大括号中的值不是同一类型), 编译会出错:
auto x5 = { 1, 2, 3.0 }; // error! 类型不一致, 不能将推断为 std::initializer_list<T>
这里会发生两种类型推断, 一种是将统一初始化式推断为 std::initializer_list, 而 std::initializer_list 本身也是一个类型为 T 的模板, 因此会根据统一初始化式中的实参对 T 进行模板类型推断, 这是第二种类型推断. 上面的类型推断会失败是因为第二种类型推断会失败.
对统一初始化式的处理的不一致是 auto 和模板类型推断的唯一区别. 使用统一初始化式对 auto 变量初始化会将其推断为 std::initializer_list, 但是模板类型推断不会这么做:
- auto x = {
- 11, 23, 9
- }; // x 的类型为 std::initializer_list<int>
- template<typename T> // 和 auto x 等同的模板类型推断
- void f(T param);
- f({
- 11, 23, 9
- }); // 错误! 这里不能推断 T 的类型.
如果要达到 auto 的效果, 得按照下面的方式来做:
- template<typename T>
- void f(std::initializer_list<T> initList);
- f({
- 11, 23, 9
- }); // T 被推断为 int, initList 的类型为 std::initializer_list<int>
在 C++11 中使用 auto 时, 这里比较容易出错, 你本来想声明别的变量, 最终却将其声明成了一个 std::initializer_list. 因此, 要谨慎使用统一初始化.
在 C++14 中, 允许将 auto 作为函数返回值, 也可以用其修饰 lambda 表达式中的参数. 但是这些 auto 使用的都是模板类型推断, 而不是 auto 类型推断, 因此一个函数返回值为 auto 类型时, 返回统一初始化式的值会出错:
- auto createInitList()
- {
- return { 1, 2, 3 }; // 错误! 不能推断 {1,2,3}
- }
下面的方式是对的:
- std::initializer_list<int> createInitList()
- {
- return { 1, 2, 3 }; //
- }
最后总结一下:
模板类型推断是 auto 的基础, auto 关键字扮演了模板类型推断中的 T, 而类型说明符扮演的是 ParamType.
对于模板类型推断和 auto 类型推断, 大多数场景下推断规则相通, 有一种特殊情况, 就是统一初始化式.
C++14 中使用 auto 可以作为函数返回值, 也可以作为 lambda 表达式的参数修饰符, 但需要注意, 这里的 auto 使用的是模板类型推断, 而不是 auto 类型推断.
来源: https://www.cnblogs.com/harlanc/p/10628321.html