您的位置:首页 > 编程语言 > C语言/C++

《Effective Modern C++》翻译--条款2: 理解auto自动类型推导

2016-01-10 19:15 771 查看
条款2: 理解auto自动类型推导

如果你已经读过条款1关于模板类型推导的内容,那么你几乎已经知道了关于auto类型推导的全部。至于为什么auto类型推导就是模板类型推导只有一个地方感到好奇。那是什么呢?即模板类型推导包括了模板、函数和参数,而auto类型推断不用与这些打交道。

这当然是真的,但是没关系。模板类型推导和auto自动类型推导是直接匹配的。从字面上看,就是从一个算法转换到另一个算法而已。

在条款1中,阐述模板类型推导采用的是常规的函数模板:

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


并且是常规方法进行调用:

[code]f(expr);    //call f with some expression


在调用f的过程中,编译器通过expr推导出T和ParamType的类型。

当使用auto关键字声明一个变量时,auto关键字就扮演者上述模板中T的角色,并且类型说明符与ParamType扮演者同样的角色。比语言描述更加清晰的是展示他们,所以请看这样的例子:

[code]auto x = 27;


这样,x的类型说明符与自己一样。另一方面,这样声明:

[code]const auto cx = x;


这里的类型说明符是const auto。而这里:

[code]const auto& rx = x;


此时类型说明符是const auto&。为了推断上面这些例子中的x,cx和rx变量,编译器起到的作用就是为每个声明提供一个模板,并且使用相应的初始化表达式来调用这些模板:

[code]template<typename T>        //conceptual template for deducing x's type
void func_for_x(T param);   

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

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

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

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

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


正如我之前所说,auto类型推导与模板类型推导是一样的。

条款1中根据ParamType和在常规模板中param的类型说明符,把模板类型推导分为三种情况。在通过auto进行变量类型推导时,类型说明符替代了ParamType,但也是分为三个情况:

•第一种情况:类型说明符是一个指针或是引用,但不是universal reference。

•第二种情况:类型说明符是一个universal reference。

•第三种情况:类型说明符既不是指针也不是引用。

我们分别看看第一和第三种情况的例子:

[code]auto x = 27;           //case 3 (x is neither ptr nor reference)

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

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


第二种情况正如你期待的那样:

[code]auto&& uref1 = x;       //x is int and 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&&


条款1中总结了对于非引用类型的说明符,数组和函数名如何退化为指针。这当然同样适用于auto类型推导:

[code]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 a 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类型推导真的和模板类型推导是一样的。就好比一枚硬币的两个面相同。

您肯定期待二者的不同。让我从观察声明一个变量并初始化为27开始,在C++98中,给了你两种语法选择:

[code]int x1 = 27;
int x2(27);


C++11中还增加了这个:

[code]int x3 = {27};
int x4{27};


总之,四种语法都得到了一个结果,就是把变量初始化为27。

但是正如条款5解释的那样,使用auto代替固定的类型来声明变量有很多的优势,所以使用auto关键字代替上面程序中的int类型。简单的文本替换我们就得到这样的代码:

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


上面的这些都可以通过编译,但是与之前的相比,代表的含义已经不同了。上面四个表达式中前两个实际上是声明一个值为27int类型的变量。而后两个,是声明一个类型为
std::initializer_list<int>
,并且具有一个值为27的元素!

[code]auto x1 = 27;        //type is int, value is 27

auto x2(27);         //同上

auto x3 = {27};      //type is std::initializer_list<int>, value is {27}

auto x4{27};         //同上


这是因为auto类型推导有特俗的规则。当为auto声明的类型进行初始化使用封闭的大括号时,则推导的类型为std::initializer_list。如果这样的类型不能被推导,这样的编码是被编译器拒绝的:

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


正如上面注释中提示的那样,这样的情况下类型推导会失败,但更重要的是认清此时采用了两种类型推导。一个是通过auto完成对x5的类型推导。由于x5是使用大括号进行的初始化,所以x5必须被推导为std::initializer_list类型。但是std::initializer_list是一个模板。对于
std::initializer_list<int>


的实例化需要推导T的类型。这样的类型推导发生第二种类型推导的管辖范围:模板类型推导。在这个例子中,类型推导失败,因为初始化值不具有单一类型。

对待使用大括号进行初始化,auto类型推导和模板类型推导是有区别的。当一个auto变量被大括号进行初始化时,推导的类型为std::initializer_list实例化的类型。如果一个模板面临着对于大括号初始化进行类型推导,这样的代码是被拒绝的。(条款32将解释完美的转发)

你会怀疑为什么对于大括号进行初始化,auto类型推导会有特殊的规则呢,而模板类型推导则没有。我自己对此也表示怀疑。不幸的是,我找不到令人信服的解释。但是规则就是规则,这也就意味着对于auto推导用大括号初始化的变量时你必须铭记于心,推导类型的总是std::initializer_list。这是特别重要的是要牢记这一点,如果你面对在大括号包围初始化值作为理所当然的初始化的理念。在C++11编程中,其中最典型的错误是你想声明一个其他类型的变量,却声明一个std :: initializer_list变量。重申一下:

[code]auto x1 = 27;    //x1 and x2 are ints
auto x2(27);

auto x3 = {27};  //x3 and x4 are std::initializer_list<int>s
auto x4{27};


这个陷阱使得一些开发者只有迫不得已的时候才使用大括号初始化变量。(我们在条款7中再讨论。)

对于C++11来说,这里是全部的内容,但是对于C++14,故事还在继续。C++14允许使用auto去推导函数的返回值(参见条款3),并且C++14中的lambda表达式在参数声明时可以使用auto类型推导。但是,这里使用的auto推导采用的是模板推导的规则,而不是auto类型推导的规则。这就意味着,使用大括号初始化将造成类型推导失败。所以使用auto作为返回类型的函数返回一个使用大括号初始化的变量编译不会通过:

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


同样的道理也适用于C++14中lambda表达式使用auto作为参数(因此产生一个通用lambda表达式):

[code]std::vector v;
....
auto resetV = [&v](const auto& newValue) { v = newValue;};//C++14 only
....
resetV({1, 2, 3});            //error! can't deduce type for {1, 2, 3}


最终的结果是,如果不使用大括号进行初始化,auto类型推导是和模板类型推导一致的。仅在这种情况下(使用大括号进行初始化),auto推导为一个std:: initializer_list,但模板类型推导会失败。

请记住:

•auto类型对象通常与模板类型推导是相同的。

•唯一的例外是:使用auto和大括号进行初始化时,自动推导为std::initializer_lists。

•对于使用括号进行初始化,模板类型推导会失败。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: