Effective Modern C++ Item 2

翻译 2017年08月03日 19:55:21

Item2:理解auto类型推导

如果你已经读过条款1关于模板类型的推导,你几乎已经知道所有关于auto类型推导的知识了,因为除了一种特殊情况,auto类型推导就是模板类型推导。但怎么会这样?模板类型推导涉及模板和函数还有参数,但auto并没有涉及这些东西。

这是真的,但没关系。在模板类型推导和auto类型推导间有一个直接匹配。从字面上来说,有一种转换到另一种的算法。

在条款1中,使用这个通用的函数模板来解释模板类型推导

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

还有这个通用调用:

f(expr);                                //用某些表达式来调用f

在这个对f的调用中,编译器用expr来推导TParamType的类型。当一个变量使用auto声明时,auto在模板中扮演T的角色,变量的类型说明符起ParamType的作用。使用代码展示比描述更容易理解,所以考虑这个例子:

auto x = 27;

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

const auto cx = x;

该类型说明符为const auto。而在这里,

const auto& rx = x;

的类型说明符为const auto&。为了推导该例中的xcxrx的类型,编译器假装对每个声明都有一个模板,此外还使用对应的初始化表达式来调用模板:

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

func_for_x(27);                         //概念上的调用:
                                        //param的类型推导为x的类型

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

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

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

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

正如我所说的,对auto的类型推导,只有一个例外(我们马上就要讨论的),其它与模板类型推导相同。

条款1将模板类型推导基于在通用函数模板中的ParamType的特性和param的类型说明符分为三种类型。在使用auto的变量声明中,类型说明符代替了ParamType,所以同样也有三种情况:

  • Case1:类型说明符是指针或引用,但不是通用引用。
  • Case2:类型说明符是通用引用。
  • Case3:类型说明符既不是指针也不是引用。

我们早已见过case1和case3的示例了:

auto x = 27;                            //case3(x既不是指针也不是引用)

const auto cx = x;                      //case3(cx也都不是)

const auto& rx = x;                     //case1(rx是非通用引用)

Case2也像你所期望的一样工作:

auto&& uref1 = x;                       //x是int类型和左值
                                        //因此uref1的类型是int&

auto&& uref2 = cx;                      //cx是const 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 func1 = someFunc;                  //func1的类型是
                                        //void (*)(int, double)

auto& func2 = someFunc;                 //func2的类型是
                                        //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声明变量比固定类型有优势,所以上述中变量声明使用auto替代int是好的。简单的文本替换产生这样的代码:

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

这些声明都可以被编译。但不都与替换前有相同的意思。前两句语句是的,确实,声明了一个值为27的int类型的变量。但是,后两个声明了只包含一个元素27的std::initializer_list<int>类型的变量!

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

auto x2(27);                            //同上

auto x3 = { 27 };                       //类型为std::initializer_list<int>
                                        //,值为{ 27 }

auto x4{ 27 };                          //同上

这是因为auto的特殊类型推导规则造成的,当对auto声明的变量使用花括号初始化时,其推导类型为std::initializer_list。如果这个类型不能被推导(例如,因为花括号初始化中的类型不同),该代码将无法编译:

auto x5 = { 1, 2, 3.0 };                //错误!不能对
                                        //std::initializer_list<T>中的
                                        //的T进行推导

正如注释所说,在这个例子中类型推导将失败,认识到这里有两种不同的类型推导是十分重要的。一种是使用autox5的类型必须被推导。因为x5的初始化是使用花括号的,x5必须被推导为std::initializer_list类型。但是std::initializer_list是一个模板。其对类型T的实例化是std::initializer_list<T>,也意味着T的类型同样要被推导。发生在这里的推导属于第二种类型推导的范畴:模板类型推导。在这个例子中,这个类型推导失败了,因为在初始化括号中不是同一个类型的值。

auto类型推导和模板类型推导唯一的不同就是处理花括号初始化的方式不同。当auto声明的变量使用花括号初始化时,推导的类型是std::initializer_list的一个实例。但与之对应的模板传入相同的初始器,类型推导会失败,代码无法编译:

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

template <typename T>                   //带参数的模板
void f(T param);                        //声明等同于x
                                        //的声明

f({ 11, 23, 9 });                       //错误!不能对T类型推导

但是,如果你指定模板的paramstd::initializer_list<T>T是未知的,则模板类型推导就会推导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匿名表达式可在参数中使用auto。但这些auto是使用模板类型推导,而不是auto类型推导。因此一个返回类型为auto的函数返回花括号初始化是无法编译的:

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

同样当auto用于C++14的匿名表达式的参数类型中也是这样:

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使用的是模板类型推导,不是auto类型推导。

Effective Modern C++:Item 2 ->弄清auto类型推断

弄清auto类型推断如果你已经读了Item 1关于模板类型推断的内容,你现在差不多已经知道了关于auto类型推断的所有知识,因为,除了有一个比较奇怪的例外,auto类型推断就是模板类型推断。但是这又是...

Effective Modern C++ Item 7 总结:关于两种对象创建方法“()、{}”的区分

在用大括号进行初始化的时候有两个例外: 例外1. 空大括号意味着无参,而不是空的std::initializer_list. 在对象的定义中会调用类的无参构造函数。 例如有如下类定义: class W...
  • Jxianxu
  • Jxianxu
  • 2017年04月17日 09:56
  • 160

Effective Modern C++ : Item 6 -> 当auto推断出不想要的类型时,使用显式类型初始化语法

当auto推断出不想要的类型时,使用显示类型初始化语法Item 5里面说了使用auto来声明变量与显式声明变量相比,有着很多的技术优势,但是有时候你想要一个zag,auto可能给你推断出一个zig。例...

Effective Modern C++》Item 3总结

Effective C++ 条款3,简单介绍下decltype。

Effective Modern C++: Item 10 -> 优先选择scoped enums而不是unscoped enums

优先选择scoped enums而不是unscoped enums作为一个基本规则,在一个大括号里面声明一个名字会将其可见性限制在大括号定义的范围内。但是对于以C++98风格enum声明的枚举器就不再...

Effective Modern C++ : Item 8 -> 优先选择nullptr而不是0和NULL

优先选择nullptr而不是0和NULL 我们知道:0是一个int,而不是一个指针。如果C++在一个只有指针才能够使用的上下文中发现它只有一个0,那么它会勉强将0解释成空指针,但那时一种倒退行为。C...

Effective Modern C++ Item 1

当一个复杂的系统的用户不知系统是如何工作的,仍对其所做的感到满意。按照这样的标准,C++中的模板类型推导是非常成功的。数以百万的程序员向模板函数传递参数并得到完全满意的结果。尽管许多程序员很难解释这些...

《Effective Modern C++》Item 1总结

理解模板类型推导总结

Effective Modern C++: Item 12 -> 声明覆盖函数override

声明覆盖函数override C++中面向对象编程的世界中主要涉及类,继承和虚函数。在这个世界中最基础的思想之一就是继承类中的虚函数实现会覆盖掉基类中的对应实现。然而,意识到虚函数覆盖多么容易出错将...

Effective Modern C++ Item 3

decltype是一个古怪的创造。给一个名字或表达式,decltype告诉你这个名字或表达式的类型。通常,它告诉你的就是你所预测的。但是偶尔,它提供的结果会让你抓破头让你求助于参考书或在线Q&A网站来...
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:Effective Modern C++ Item 2
举报原因:
原因补充:

(最多只允许输入30个字)