💎一站式轻松地调用各大LLM模型接口,支持GPT4、智谱、星火、月之暗面及文生图 广告
条款二:理解`auto`类型推导 ========================= 如果你已经阅读了条款1关于模板相关的类型推导,你就已经知道了机会所有关于`auto`的类型推导,因为除了一个例外,`auto`类型推导就是模板类型推导。但是它怎么就会是模板类型推导呢?模板类型推导涉及模板和函数以及参数,但是`auto`和上面的这些没有任何的关系。 这是对的,但是没有关系。模板类型推导和`auto`类型推导是有一个直接的映射。有一个书面上的从一种情况转换成另外一种情况的算法。 在条款1,模板类型推导是使用下面的通用模板函数来解释的: ```cpp template<typename T> void f(ParamType param); ``` 在这里通常调用: ```cpp f(expr); // 使用一些表达式来当做调用f的参数 ``` 在调用`f`的地方,编译器使用`expr`来推导`T`和`ParamType`的类型。 当一个变量被声明为`auto`,`auto`相当于模板中的`T`,而对变量做的相关的类型限定就像`ParamType`。这用代码说明比直接解释更加容易理解,所以看下面的这个例子: ```cpp auto x = 27; ``` 这里,对`x`的类型定义就仅仅是`auto`本身。从另一方面,在这个声明中: ```cpp const auto cx = x; ``` 类型被声明成`const auto`,在这儿: ```cpp const auto& rx = x; ``` 类型被声明称`const auto&`。在这些例子中推导`x`,`cx`,`rx`的类型的时候,编译器处理每个声明的时候就和处理对应的表达式初始化的模板: ```cpp 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的例子: ```cpp auto x = 27; // 情况3(x既不是指针也不是引用) const auto cx = x; // 情况3(cx二者都不是) const auto& rx = x; // 情况1(rx是一个非通用的引用) ``` 情况2正如你期待的那样: ```cpp auto&& uref1 = x; // x是int并且是左值 // 所以uref1的类型是int& auto&& uref2 = cx; // cx是int并且是左值 // 所以uref2的类型是const int& auto&& uref3 = 27; // 27是int并且是右值 // 所以uref3的类型是int&& ``` 条款1讲解了在非引用类型声明里,数组和函数名称如何退化成指针。这在`auto`类型推导上面也是一样: ```cpp 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你有两种语法选择: ```cpp int x1 = 27; int x2(27); ``` C++11,通过标准支持的统一初始化(使用花括号初始化——译者注),可以添加下面的代码: ```cpp int x3 = { 27 }; int x4{ 27 }; ``` 综上四种语法,都会生成一种结果:一个拥有27数值的`int`。 但是正如条款5所解释的,使用`auto`来声明变量比使用固定的类型更好,所以在上述的声明中把`int`换成`auto`更好。最直白的写法就如下面的代码: ```cpp auto x1 = 27; auto x2(27); auto x3 = {27}; auto x4{ 27 }; ``` 上面的所有声明都可以编译,但是他们和被替换的相对应的语句的意义并不一样。头两个的确是一样的,声明一个初始化值为27的`int`。然而后面两个,声明了一个类型为`std::intializer_list<int>`的变量,这个变量包含了一个单一的元素27! ```cpp auto x1 = 27; // 类型时int,值是27 auto x2(27); // 同上 auto x3 = { 27 }; // 类型是std::intializer_list<int> // 值是{ 27 } auto x4{ 27 }; // 同上 ``` 这和`auto`的一种特殊类型推导有关系。当使用一对花括号来初始化一个`auto`类型的变量的时候,推导的类型是`std::intializer_list`。如果这种类型无法被推导(比如在花括号中的变量拥有不同的类型),代码会编译错误。 ```cpp 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`的一个实例。但是如果相同的初始化递给相同的模板,类型推导会失败,代码不能编译。 ```cpp 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>`: ```cpp 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`声明的返回值的函数,返回一个花括号初始化就无法编译。 ```cpp auto createInitList() { return { 1, 2, 3 }; // 编译错误:不能推导出{ 1, 2, 3 }的类型 } ``` 在C++14的lambda里面,当`auto`用在参数类型声明的时候也是如此: ```cpp std::vector<int> v; … auto resetV = [&v](const auto& newValue) { v = newValue; } // C++14 … resetV({ 1, 2, 3 }); // 编译错误,不能推导出{ 1, 2, 3 }的类型 ``` |要记住的东西| | :--------- | |`auto`类型推导通常和模板类型推导类似,但是`auto`类型推导假定花括号初始化代表的类型是`std::initializer_list`,但是模板类型推导却不是这样| |`auto`在函数返回值或者lambda参数里面执行模板的类型推导,而不是通常意义的`auto`类型推导|