Effective Modern C++ Item 2

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类型推导。
阅读更多
文章标签: c++
个人分类: C++ 翻译
想对作者说点什么? 我来说一句

没有更多推荐了,返回首页

关闭
关闭
关闭