[译] Item 2: Understand Auto Type Deduction

如果你已经阅读了 Item 1 模板类型推导,那么你应该已经掌握了 auto 的类型推导,出了下面要讲到的一个不同之外,其他都和 Item 1 完全一致,但是你肯定还有疑问,为什么模板类型推导涉及到了模板,函数以及参数,而 auto 却不涉及这些。

没错,但是这也没什么关系。其实模板类型推导与 auto 类型推导有直接的映射关系。有很直观的转换关系。

在 Item 1 中,我们使用下面的模板函数用来描述模板类型推导,

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

调用如下:

1
f (expr);    // call f with some expression

在调用函数 f 时,编译器使用表达式 expr 来推导类型 TParamType

当使用 auto 来声明变量时,auto 代替了上面 T 的位置,同时变量的类型就是 ParamType 的类型。看下面的例子会更直观,

1
auto x = 27;

在这里,变量 x 的类型标识符就是一个简单的 auto,在

1
const auto cx = x;

中,类型标识符是 const auto,在

1
const auto& rx = x;

中,类型标识符是 const auto&。推导上面 x, cx 以及 rx 的类型,编译器所做的事情就像是有一个模板函数一样,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
template <typename T>
void func_for_x(T param);           // conceptual template for deducing x's type

func_for_x(27);                     // conceptual call: param's deduced type is x's type

template <typename T>
void func_for_cx(const T param);    // conceptual template for deducing cx's type

func_for_cx(x);                     // conceptual call: param's deduced type is cx's type

template <typename T>
void func_for_rx(const T& param);   // conceptual template for deducing rx's type

func_for_rx(x);                     // conceptual call: param's deduced type is rx's type

就像刚才说的一样,auto 的类型推导只有一点(一会儿会讲到)不一样之外,其他与模板类型推导完全一致。

Item 1 中根据 ParamType 的类型,把类型推导分了三种情况来处理,在 auto 类型推导时,auto 替代了 ParamType 也同样分为三种情况,

  • 情况1:类型标识符是一个指针或者引用,但不是右值引用
  • 情况2:类型标识符是一个全局引用
  • 情况3:类型标识符既不是指针也不是引用

我们已经见过了情况1 与情况3 的例子了,

1
2
3
4
5
auto x = 27;           // case 3 (x is neither ptr nor reference)

const auto cx = x;     // case 3 (cx isn't neither)

const auto& rx = x;    // case 1 (rx is a non-universal ref.)

情况2 就像你预期的那样,

1
2
3
4
5
auto&& uref1 = x;     // x is int lvalue, so uref1's type is int&

auto&& uref2 = cx;    // cx is const int and lvalue, so uref2's type is const int&

auto&& uref3 = 27;    // 27 is int and rvalue, so uref3's type is int&&

Item 1 中我们讨论了非引用的数组和函数是如何退化成指针的。在 auto 类型推导中也是一样的,

1
2
3
4
5
6
7
8
9
10
11
const char name[] = "R. N. Briggs";    // name's type is const char[13]

auto arr1 = name;                      // arr1's type is const char *

auto& arr2 = name;                     // arr2's type is const char (&)[13]

void someFunc(int, double);            // someFunc is function, type is void(int, double)

auto func1 = someFunc;                 // func1's type is void(*)(int, double)

auto& func2 = someFunc;                // func2's type is void(&)(int, double)

就像你看到这样,auto 类型推导就像模板类型推导一样。

仅有一种情况,他们是不一样的。我们从一个简单的例子开始,在 C++98 中,我们用 27 来初始化一个 int 变量,我们可以有下面两种写法,

1
2
int x1 = 27;
int x2(27);

C++11 统一初始化,增加了下面的写法,

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

总而言之,4中不同的写法的结果都是一样的,初始化了一个值为 27 的整型。

但是,就像 Item 5 中解释的一样,使用 auto 来声明类型是可以获得好处的,所有我们可以将上面的 int 全部替换为 auto

1
2
3
4
auto x1 = 27;
auto x2(27);
auto x3 = { 27 };
auto x4{ 27 };

这 4 种写法都能够正常编译,但是却表达了不同的含义。前两种写法定义了一个值为 27 的整型。但后两种写法实际上定义了一个只有一个元素 27 的 std::initializer_list<int>

1
2
3
4
auto x1 = 27;        // type is int, value is 27
auto x2(27);         // ditto
auto x3 = { 27 };    // type is std::initializier_list<int>, value is { 27 }
auto x4{ 27 };       // ditto

这是 auto 类型推导的一个特殊规则。当使用大括号初始化一个 auto 变量时,类型会被推导为 std::initializer_list,如果类型推导不成功(比如,大括号中的某个元素类型与其他的不一致),那么将会编译失败:

1
auto x5 = { 1, 2, 3.0 };    // error! can't deduce T for std::initializer_list<int>

就像上面注释中写的一样,类型推导在这种情况下会失败,但是要明白这里有两个类型推导,这是很重要的。第一个是 autox5 使用了大括号来初始化,那么 auto 会被推导为 std::initializer_list 类型,但同时 std::initializer_list 是一个模板类型,模板参数 T 同样需要推导,类型推导失败产生于第二步:模板类型推导。在上面的例子中,就是由于大括号中的元素类型不一致而导致的模板类型推导失败。

auto 类型推导与模板类型推导的唯一不同在于大括号初始化。当使用大括号初始化时,auto 会推到为 std::initializer_list 类型,但对于模板类型推导来说,这会产生一个错误,

1
2
3
4
5
6
auto x = { 11, 23, 9 };  // x's type is std::initializer_list<int>

template<typename T>
void f(T param);         // template with parameter declaration equivalent to x's

f({ 11, 23, 9});         // error! can't deduce type for T

不过,如果你明确指定了 param 的类型为 std::initializer_list<T> 那么模板类型推导会推导出 T 的类型,

1
2
3
4
template<typename T>
void f(std::initializer_list<T> initList);

f({ 11, 23, 9 });    // T deduce as int, and initList's type is std::initializer_list<int>

因此,auto 类型推导与模板类型推导唯一的不同在于,对于大括号初始化,auto 类型推导会将它推导为 std::initializer_list,而模板类型推导不会。

也许你会想知道为什么会有这样的区别。我也曾想过,但是没有找到一个很好的解释。不过规则就是规则,你只需要记住,当使用大括号初始化 auto 类型时,它会被自动推到为 std::initializer_list 类型。如果你想要使用新的统一初始化,那么记住这一点是非常重要的。一个典型的 C++11 错误就是当你想要一个其他类型时却意外的声明了一个 std::initializer_list 类型。这也是为什么有些开发者仅仅在必要的时候才使用大括号初始化的一个原因。(我们会在 Item 7 中详细讨论)

对 C++11 来说,这就是全部了。但是对于 C++14,还有些其他内容。C++14 允许使用 auto 来表示需要被推导的函数返回值类型(详见 Item 3),C++14 的 lambda 中也允许 auto 用作参数类型。但是这些 auto 遵循的是模板类型推导,而不是 auto 类型推导。因此,一个返回值类型声明为 auto 返回一个大括号初始化时,会导致编译失败,

1
2
3
4
auto createInitList()
{
  return { 1, 2, 3 };    // error: can't deduce type for { 1, 2, 3 }
}

lambda:

1
2
3
4
5
6
std::vector<int> v;
// ...
auto resetV = [&v](const auto& newValue) { v = newValue; };    // C++14
// ...

resetV({ 1, 2, 3 });    // error! can't deduce type for { 1, 2, 3 }

需要记住的

  • auto 类型推导通常和模板类型推导是一致的,但是 auto 类型推导会将大括号初始化推到为 std::initializer_list,而模板类型推导不会
  • auto 在函数返回值以及 lambda 参数类型推导时,遵循模板类型推导,而不是 auto 类型推导。

Comments