• Effective Modern C++ 条款2:理解auto型别推导


      在条款1中,我们已经了解了有关模板型别的推导的一切必要知识,那么也就意味着基本上了解了auto型别推导的一切必要知识。

      因为,除了一个奇妙的例外情况,auto型别推导就是模板型别推导。尽管和模板型别推导打交道的是模板、函数和形参,auto和它们秋毫无犯,但并不影响上面的结论成立。

      在条款1中,我们用来解释模板型别推导的函数模板形如: 

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

      而一次调用形如:

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

      在f的调用语句中,编译器会利用expr来推导T和ParamType的型别。

      当某变量采用auto来声明时,auto就扮演了模板中的T这个角色,而变量的型别饰词则扮演的是ParamType的角色。

      所以,除了一种例外情况(马上要讨论),和为模板推导型别一模一样。

      条款1根据ParamType的特征,即一般形式的函数模板中param的型别饰词,将模板推导分为三种情况。而在采用auto进行变量声明中,型别饰词取代了ParamType,所以也存在三种情况:

      情况1:型别饰词是指针或引用,但不是万能引用;

      情况2:型别饰词是万能引用;

      情况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的型别是int&
    
    auto&& uref2 = cx;  //cx的型别是const int,且是左值,
                                  //所以uref2的型别是const int&
    
    auto&& uref2 = 27;  //27的型别是int,且是右值,
                                  //所以uref3的型别是int&&

      条款1中数组和函数名称在非引用型别饰词的前提下退化成指针,同样的结论也适用于auto型别推导:

    const char name[] = "Hello World";    //name的型别是const char[12]
    
    auto arr1 = name;    //arr1的型别是const char*
    
    auto& arr2 = name;    //arr2的型别是const char(&)[12]
    
    
    void someFunc(int ,double);    //someFunc是个函数,
                                   //其型别是void(int ,double)
    auto func1 = someFunc; //func1的型别是void(*)(int, double) auto& func2 = someFunc; //func2的型别是void(&)(int, double)

      auto型别推导和模板型别推导的运作是类似的。

      下面我们讨论只有一个不同之处。我们观察一下,若要声明int,并将其初始化为值27,C++98中有两种可选语法:

    int x1 = 27;
    int x2(27);

      而C++11为了支持统一初始化,增加了下面的语法规则:

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

      共计四种语法,然而结果却殊途同归:得到一个值为27的int。

      采用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的一条特殊的型别推导规则所致。当用于auto声明变量的初始化表达式是使用大括号括起来时,推导所得的型别就属std::initializer_list<int>。

      这么一来,如果型别推到失败(例如,大括号里的值型别不一),则代码就通不过编译:

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

      正如注释所指出的那样,这种情况下型别推导会失败,但重点在于,要意识到这里发生了两种型别推导。第一种源于auto的使用:x5的型别需要推导。而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的型别

      所以,auto和模板型别推导真正的唯一区别在于,auto会假定用大括号括起来的初始化表达式代表一个std::initializer_list,但模板型别推导却不会。

      不过,若指定模板中的param为std::initializer_list<T>,则在T的型别未知时,模板型别推导机制会推导出T应有的型别:

    template<typename T>       //带有形参的模板
    void f(std::initializer_list<T> param);        
    
    f({ 11, 23, 9});     //T的型别推导为int
                            //从而initList的型别std::initializer_list<int>

      关于C++11,上面所说的就是全部了。但是关于C++14,还有别的要说。C++14允许使用auto来说明函数返回值需要推导,而且C++14中的lambda式也会在形参声明中用到auto。然而,这些auto用法是在使用模板型别推导而非auto型别推导。所以带有auto返回值的函数若要返回一个大括号括起的初始化表达式,是通不过编译的:

    auto createInitList()
    {
          return { 1, 2, 3}; //错误:无法为{ 1, 2, 3}完成型别推导
    }

      同样地,用auto来制定C++14中lambda式的形参型别时,也不能使用大括号括起的初始化表达式:

    std::vector<int> v;
    ...
    
    auto resetV = 
        [&v](const auto& newValue) { v = newValue; };  // C++14
    
    ...
    
    resetV( {1,2,3});//错误!无法为{1,2,3}完成型别推导

    要点速记:

    1、在一般情况下,auto型别推导和模板型别推导时一模一样的,但是auto型别推导会假定用大括号括起的表达式代表一个std::initializer_list,但模板型别推导却不会。

    2、在函数返回值或lambda式的形参中使用auto,意思是使用模板型别推导而非auto型别推导。

  • 相关阅读:
    java 堆栈 附图
    synchronized、volatile关键字
    Swift随笔
    java |、&、~、>>、<<运算符的作用。
    java双向链表示意图
    java单链表
    List集合的过滤之lambda表达式
    SQL hint作用
    创建触发器的一般语法
    多线程创建方式
  • 原文地址:https://www.cnblogs.com/malinzhai/p/10580905.html
Copyright © 2020-2023  润新知