Effective Modern C++翻译(3)-条款2

条款2 明白auto类型推导

如果你已经读完了条款1中有关模板类型推导的内容,那么你几乎已经知道了所有关于auto类型推导的事情,因为除了一个古怪的例外,auto的类型推导规则和模板的类型推导规则是一样的,但是为什么会这样呢?模板的类型推导涉及了模板,函数和参数,但是auto的类型推导却没有涉及其中的任何一个。

这确实是对的,但这无关紧要,在auto类型推导和template之间存在一个直接的映射,可以逐字逐句的将一个转化为另外一个。

在条款1中,模板类型推导是以下面的模板形式进行举例讲解的:

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

函数调用是这样

f(expr); //用一些表达式调用函数f

在f的函数调用中,编译器使用expr来推导T和ParamType的类型。

当一个变量用auto进行声明的时候,auto扮演了模板中的T的角色,变量的类型说明符(The type specifier)相当于ParamType,这个用一个例子来解释会更容易一些,考虑下面的例子:

auto x=27;

这里x的类型说明符就是auto本身,在另一方面,在下面这个声明中:

const auto cx=x;

类型说明符是const auto。

const auto& rx=x;

类型说明符是const auto&,在上面的例子中,为了推导x,cx,rx的类型,编译器会假装每一个声明是一个模板,并且用相应的初始化表达式来调用(compilers act as if there were a template for each declaration as well as a call to that template with the corresponding initializing expression:)

template<typename T>              // 产生概念上的模板来
void func_for_x(T param);         // 推导x的类型
func_for_x(27);                   // 概念上的函数调用,参数
                                  // 推导出的类型就是x的类型
template<typename T>              // 产生概念上的模板来
void func_for_cx(const T param);  // 推导cx的类型
func_for_cx(x);                   // 概念上的函数调用,参数
                                  // 推导出的类型就是cx的类型
template<typename T>              // 产生概念上的模板来
void func_for_rx(const T& param); // 推导cx的类型
func_for_rx(x);                   // 概念上的函数调用,参数
                                  // 推导出的类型就是rx的类型

就像我说的那样,auto的类型推导和模板的类型推导是一样的。

条款1把模板的类型推导按照ParamType的类型,分成了3种情况,同样,在auto声明的变量中,变量的类型说明符(The type specifier)相当于ParamType,所以auto类型推导也有3种情况:

  • 情况1:类型说明符是一个指针或是一个引用,但不是一个万能引用(universal reference)
  • 情况2:类型说明符是一个万能引用(universal reference)
  • 情况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的类型是
auto&& uref2 = cx;   // cx的类型是const int并且是一个左值
                     // 所以uref2的类型是const int&

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

条款1同样也讨论了数组和函数名在非引用类型的类型说明符下,会退化为指针类型,这当然同样适用于auto的类型推导

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

auto arr1 = name;                   //arr1的类型是const char*
auto& arr2 = name;                  //arr2的类型是
                                    // const char (&)[13]
void someFunc(int, double);         //someFunc是一个函数;
                                    //类型是void(int, double)
auto func1 = someFunc;              // func1的类型是
                                    // void (*)(int, double)
auto& func2 = someFunc;             // func2的类型是
                                    // void (&)(int, double)

就像你看到的那样,auto类型推导其实和模板类型推导是一样的,他们就相当于硬币的正反两个面。

但是在一点上,他们是不同的,如果你想把一个声明一个变量,它的初始值是27,C++98中,你可以使用下面的两种语法

int x1 = 27;
int x2(27);

在C++11中,提供对统一的集合初始化(uniform initialization)的支持,增加下面是声明方式。

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

总而言之,上面的4种声明方式的结果是一样的,声明了一个变量,它的初始值是27。

但是就像条款5解释的那样,使用auto声明变量要比使用确定的类型声明更有优势,所以将上面代码变量声明中的int替换成auto会是非常好的,直接的文本上的替换产生了下面的代码:

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

这些声明都能够通过编译,但他们并非全和替代前有着同样的意义,前两个的确声明了一个int类型的变量,初始值为27;然而,后两个声明了一个std::initializer_list<int>类型的变量,它包括一个元素,初始值是27;

auto x1 = 27;    // 类型是int,初始值是27
auto x2(27);     // 同上
auto x3 = {27};  //类型是std::initializer_list<int>
                 //初始值是27
auto x4{27};     //同上

这是由于auto类型推导的一个特殊的规则,当变量使用大括号的初始化式(braced initializer)初始化的时候,被推导出的类型是std::initializer_list,如果这个类型不能被推导出来(比如,大括号的初始化式中的元素有着不同的类型),代码将不能通过。

auto x5 = {1, 2, 3.0}; // 错误!无法推导出std::initializer_list<T>中T的类型

就像注释里指出的的那样,类型推导在这种情况下失败了,但是,重要的是认识到这里其实发生了两种形式的类型推导,一种来源于auto的使用,x5的类型需要被推导出来,另外因为auto是用大括号的初始化式初始化的,x5的类型必须被推导为std::initializer_list,但是std::initializer_list是一个模板,所以实例化模板std::initizalizer_list<T>意味着T的类型必须被推导出来,在上面的例子中,模板的类型推导失败了,因为大括号里变量类型不是一致的。

对待大括号的初始化式(braced initializer)的不同是auto类型推导和模板类型推导的唯一区别,当auto变量用一个大括号的初始化式(braced initializer)初始化的时候,推导出的类型是实例化后的std::initializer_list模板的类型,而模板类型推导面对大括号的初始化式(braced initializer)时,代码将不会通过(这是由于完美转发perfect forwarding的结果,将在条款32中进行讲解)

你可能会猜想为什么auto类型推导对于大括号的初始化式(braced initializer)有着特殊的规则,而模板类型推导确没有,我也想知道,不幸的是,我没有找到一个吸引人的解释,但是规则就是规则,这意味着,你必须记住如果你用auto声明一个变量,并且用大括号的初始化式进行初始化的时候,推导出的类型总是std::initializer_list,如果你想更深入的使用统一的集合初始化时,你就更要牢记这一点,(It’s especially important to bear this in mind if you embrace the philosophy of uniform initialization of enclosing initializing values in braces as a matter of course.)C++11的一个最经典的错误就是程序员意外的声明了一个std::initializer_list类型的变量,但他们的本意却是想声明一个其他类型的变量。让我再重申一下:

auto x1 = 27;    // x1和 x2都是int类型
auto x2(27);
auto x3 = {27};  // x3和x4是
auto x4{27};     // std::initializer_list<int>类型

陷阱的主要原因是一些程序员只有当必要的时候,才使用大括号的初始化式进行初始化)(This pitfall is one of the reasons some developers put braces around their initializers only when they have to. (什么时候你必须时候将在条款7中讨论)

对于C++11,这已经是一个完整的故事了,但是对于C++14,故事还没有结束,C++14允许auto来指出一个函数的返回类型需要被推导出来(见条款3),C++14的lambda表达式可能需要在参数的声明时使用auto,不管怎样,这些auto的使用,采用的是模板类型推导的规则,而不是auto类型推导规则,这意味着,大括号的初始化式会造成类型推导的失败,所以一个带有auto返回类型的函数如果返回一个大括号的初始化式将不会通过编译。

auto createInitList()
{
return { 1, 2, 3 };  // 错误: 无法推导出
}                    // { 1, 2, 3 }的类型

同样,规则也适用于当auto用于C++14的lambda(产生一个通用的lambda(generic lambda))的参数类型说明符时,

std::vector v;

auto resetV =
[&v](const auto& newValue) { v = newValue; }; //只在C++14下允许
…
resetV( { 1, 2, 3 } );                        //错误! 无法推导出
                                              //{ 1, 2, 3 }的类型

最终结果是auto类型推导和模板类型推导是完全相同的,除非(1)一个变量被声明了,(2)它的初始化是用大括号的初始化式进行初始化的(its initializer is inside braces),只有这种情况下,auto下被推导为std::initializer_list,而模板会失败。

请记住:

  • auto的类型推导通常和模板类型推导完全相同。
  • 唯一的例外是,当变量用auto声明,并且使用大括号的初始化式初始化时,auto被推导为std::initializer_list。
  • 模板类型推导在面对大括号的初始化式(braced initializer)初始化时会失败。
时间: 2024-11-08 21:43:24

Effective Modern C++翻译(3)-条款2的相关文章

Effective Modern C++翻译(5)-条款4

条款4:了解如何观察推导出的类型 那些想要知道编译器推导出的类型的人通常分为两种,第一种是实用主义者,他们的动力通常来自于软件产生的问题(例如他们还在调试解决中),他们利用编译器进行寻找,并相信这个能帮他们找到问题的源头(they're looking for insights into compilation that can help them identify the source of the problem.).另一种是经验主义者,他们探索条款1-3所描述的推导规则,并且从大量的推导情

Effective Modern C++翻译(2)-条款1

第一章 类型推导 C++98有一套单一的类型推导的规则:用来推导函数模板,C++11轻微的修改了这些规则并且增加了两个,一个用于auto,一个用于decltype,接着C++14扩展了auto和decltype可以使用的语境,类型推导的普遍应用将程序员从必须拼写那些显然的,多余的类型的暴政中解放了出来,它使得C++开发的软件更有弹性,因为在某处改变一个类型会自动的通过类型推导传播到其他的地方.   然而,它可能使产生的代码更难观察,因为编译器推导出的类型可能不像我们想的那样显而易见.   想要在

Effective Modern C++翻译(7)-条款6:当auto推导出意外的类型时,使用显式的类型初始化语义

条款6:当auto推导出意外的类型时,使用显式的类型初始化语义 条款5解释了使用auto来声明变量比使用精确的类型声明多了了很多的技术优势,但有的时候,当你想要zag的时候,auto可能会推导出了zig.例如,我有一个函数,它以const Widget&作为参数,并且返回std::vector<bool>,每一个bool暗示了Widget是否提供了一个特殊的特性. std::vector<bool> features(const Widget& w); 进一步假设第

Effective Modern C++翻译(4)-条款3

条款3 了解decltype decltype是一个有趣的东西,给它一个变量名或是一个表达式,decltype会告诉你这个变量名或是这个表达式的类型,通常,告诉你的结果和你预测的是一样的,但是偶尔的结果也会让你挠头思考,开始找一些参考资料进行研究,或是在网上寻找答案.   我们从典型的例子开始,因为它的结果都是在我们预料之中的,和模板类型推导与auto类型推导相比(参见条款1和条款2),decltype几乎总是总是返回变量名或是表达式的类型而不会进行任何的修改 const int i = 0;

决定干点事儿--翻译一下《effective modern c++》

写了很多关于C++11的博客,总是觉得不踏实,很多东西都是东拼西凑.市场上也很少有C++11的优秀书籍,但幸运的是Meyers老爷子并没有闲赋,为我们带来了<effective modern c++>. 我们都要认清,一个人很难超越自我,超越自我的巅峰之作.因为不同的时代,也会早就不同的伟大作品. 说上面这段话的意思就是,我们不能期待<effective modern c++>能达到<effective c++>给我们带来的惊喜,但是也是出自大师之手. Learn ho

Effective Modern C++ 读书笔记 Item 1

最近发现了<Effective Modern C++>这本书,作者正是大名鼎鼎的Scott Meyers——<Effective C++>.<Effective STL>的作者. 而就在C++11逐渐普及,甚至是C++14的新特性也进入大家的视野的时候,<Effective Modern C++>一书应运而生.此书与其前辈一样,通过数十个条款来展开,只不过这次是集中于C++11和C++14的新特性.auto.decltype.move.lambda表达式……

[C++11] Effective Modern C++ 读书笔记

本文记录了我读Effective Modern C++时自己的一些理解和心得. item1:模板类型推导 1)reference属性不能通过传值参数传入模板函数.这就意味着如果模板函数需要一个reference类型的参数,必须在模板声明中将其声明为reference,否则,即使使用一个reference类型的变量调用模板函数,类型推导的结果将不带reference属性. 2)constant和volatile属性也不能通过传值参数传入模板函数,但是可以通过reference参数传入这些属性. 3

Effective C++_笔记_条款08_别让异常逃离析构函数

(整理自Effctive C++,转载请注明.整理者:华科小涛@http://www.cnblogs.com/hust-ghtao/) C++并不禁止析构函数吐出异常,但它不鼓励你这样做.考虑如下代码: 1: class Widget{ 2: public: 3: ... 4: ~Widget() {...} //假设这个可能吐出一个异常 5: }; 6:  7: void doSomething() 8: { 9: vector<Widget> v ; //v在这里被自动销毁 10: ...

Effective C++_笔记_条款12_复制对象时勿忘其每一个成分

(整理自Effctive C++,转载请注明.整理者:华科小涛@http://www.cnblogs.com/hust-ghtao/) 编译器会在必要时候为我们的classes创建copying函数,这些“编译器生成版”的行为:将被烤对象的所有成员变量都做一份拷贝. 如果你声明自己的copying函数,意思就是告诉编译器你并不喜欢缺省实现中的某些行为.编译器仿佛被冒犯似的,会以一种奇怪的方式回敬:当你的实现代码几乎必然出错时却不告诉你.所以自己实现copying函数时,请遵循一条规则:如果你为c