Modern C++类型推导

函数模板类型推导

  1. 根据函数模板传入的实参确定形参类型和模板参数类型T具体要分三种情形讨论:

    • 形参具有指针或引用类型,但不是个万能引用
    • 形参是个万能引用
    • 形参既非引用也非指针
  2. 当形参是个指针或引用但不是万能引用时,类型推导会这样运作:

    • 若实参具有引用类型,先将实参的引用部分忽略
    • 接着对实参和形参的类型执行模式匹配,来决定模板参数T的类型

    比如以下代码:

    template<typename T>
    void f(T& param);//形参param是个引用
    int x=27;//x为int类型
    const int cx=x;//cx为const int类型
    const int &rx=x;//rx是x的类型为const int的引用
    

    因此f(x)这一函数调用中,形参类型为int,模板参数T类型也为int,f(cx)推导出的形参类型为const int&,T类型为const int,f(rx)推导时会忽略实参本身的引用属性,因此推导出的形参类型和模板参数类型与f(cx)一样。

    当我们将形参类型由T&改为const T&时,由于形参本身带有const属性,因此实参本身自带的const属性将和引用属性一样被忽略,推导出的T类型将会是int。当形参为指针或是右值引用时函数模板推导作用方式与上述一样。

  3. 当形参类型为万能引用时,万能引用形参声明方式与右值引用类似(即在函数模板中持有型别形参T时,万能引用的声明型别写作T&&):

    • 如果传入实参是个左值,函数形参和模板形参T都会被推导为左值引用,这是模板型别推导中T被推导为引用类型的唯一情形,尽管声明时使用的是右值引用语法,它的类型推导结果却是左值引用
    • 如果实参是个右值,则应用上述2里的规则。
    template<typename T>
    void f(T&& param);//形参param是个引用
    int x=27;//x为int类型
    const int cx=x;//cx为const int类型
    const int &rx=x;//rx是x的类型为const int的引用
    f(x);//x是左值,所以T类型是int&,形参类型是int&
    f(cx);//cx是左值,所以T的型别是const int&,形参型别也是const int&
    f(rx);//rx是左值,所以T的型别是const int&,形参型别也是const int&
    f(27);//27是右值,如同上述2里的规则,T是int,形参型别是int&&
    

    当遇到万能引用时,类型推到规则会区分实参是左值还是右值,而非万能引用是从来不会作这样的区分的。

  4. 当形参类型既非指针也非引用时,此时函数就是所谓的按值传递,无论传入的是什么,形参都会是它的一个副本,也就是一个全新的对象:

    • 当实参具有引用类型时忽略
    • 若实参是个const对象,也忽略const属性,若有volatile属性(volatile对象不常用一般仅用于实现设备驱动程序)也忽略。

    因此之前所举的例子,无论传入的实参是否是引用是否是常量对象,推导出的函数形参和模板形参都是int类型。

    当实参是一个指向const对象的const指针且按值传递给函数模板时,此时函数形参和模板形参都会被推导为const type*,即指向一个常量对象的指针,指针本身的常量属性被忽略。

  5. 当使用数组形参时,如果函数模板使用的是按值传递,那么数组将会退化成指向其首元素的指针,而模板模板使用引用方式传递实参时,模板参数类型将会被推导为真正的数组类型,同时实参类型会被推导为数组的引用。

  6. 当使用函数形参时,若按值传递则形参被推导为函数指针,而按引用传递则会被推导为函数引用。

auto类型推导

  1. 除了大括号初始化表达式的处理方式之外,auto型别推导就是模板型别推导。当其变量采用auto来声明时,auto就扮演了模板中T的角色,而变量的型别修饰词就扮演函数形参的角色。因此,也存在三种情况:

    • 类型饰词是指针或引用,但不是万能引用
    • 类型饰词是万能引用
    • 类型饰词既非指针也非引用

    这三种情况都与上述模板类型推导出的三种情况相同,数组和函数作为实参的情况二者也是相同的。

  2. auto有一条特殊的型别推导规则,当用于auto声明变量的初始化表达式是使用大括号括起时,推导所得的型别就属于std::initializer_list但是如果向对应的模板传入一个同样的初始化表达式,类型推导就会失败。因此,auto和模板型别推导真正的唯一区别在于,auto会假定用大括号括起的初始化表达式代表一个std::initializer_list,但模板型别推导不会。但是如果指定模板中的形参类型为stdZ::initializer_list<T>,则在T的类型未知时,模板型别推导机制会推导出T应有的类别。

  3. C++14允许使用auto来说明函数返回值需要推导,14中的lambda表达式的形参类别也可以使用auto指定,但是这时的类型推导使用的是模板类型推导而不是auto类型推导,因此不能使用大括号括起来的初始化表达式作为实参。

decltype关键字的理解

  1. 对于给定的名字或表达式,decltype能告诉你该名字或表达式的类型。

  2. C++11中decltype的主要用途在于声明那些返回值类别依赖于形参类型的函数模板,比如

    template<typename Container,typename Index>
    auto authAndAccess(Container& c,Index i)->decltype(c[i]){
    authenticateUser();return c[i];
    }
    

    这里的auto是为说明这里使用了C++11中的返回值型别尾序语法,尾序返回值的好处在于在指定返回值类型时可以使用函数形参。

  3. C++11允许对单表达式的lambda式的返回值类型实施推导,而14则把这个允许范围扩张到了一切lambda式和一切函数。意味着在14中可以去掉返回值类型推导语法而只保留前导auto。但是问题隐患在于使用auto推导会忽略实参的引用属性,导致返回值类型不是引用类型。因此就需要使用decltype(auto)。代码如下:

    template<typename Container,typename Index>
    decltype(auto) authAndAccess(Container& c,Index i){
    authenticateUser();return c[i];
    }
    

    auto指定了欲推导的类型,而推导过程使用decltype的规则。

  4. 此时容器的传递方式是对非常量的左值引用,这意味着无法向该函数传递右值容器,避免同时声明两个不同形参版本的重载函数的一个方法是使用万能引用,对万能引用要应用std::forward

    template<typename Container,typename Index>
    decltype(auto) authAndAccess(Container&& c,Index i){
    authenticateUser();return std::forward<Container>(c)[i];
    }
       
    
  5. 将decltype应用于一个名字之上,就会得出该名字的声明类型,但是如果是比仅有名字更复杂的左值表达式的话,decltype就会保证得出的类型总是左值引用。只要一个左值表达式不仅是一个型别为T的名字,它就得到一个T&型别。但是这种行为的一个后果是:

    int x=0;
    decltype(x)结果是int
    decltype((x))结果就变成了int&
    

    将这个规则与decltype(auto)结合我们很可能会返回局部变量的引用这一未定义行为。

查看类型推导结果

  1. IDE编译器,作用于简单类型时信息还算准确,复杂类型时就不太有用
  2. 编译器诊断信息:想要让编译器显示其推导出的型别,一条有效途径是使用该型别导致某些编译错误,而报告错误的消息几乎肯定会提及该错误的类型。
  3. 运行时输出,使用typeid(x).name()也能输出比较简单的类型推导,但是推导复杂类型时这个方法并不可靠,因为std::type_info::name中处理类型的方式就像是向函数模板按值传递形参一样,引用和const、volatile属性都会被忽略,导致显示出的函数形参类型与模板形参类型不正确。
  4. 还可以使用boost库中的boost::typeindex::type_ide_with_cvr方法,该方法接受一个型别实参,并且不会移除const、volatile和引用饰词,该函数模板返回一个boost::typeindex::type_index对象,利用其成员函数pretty_name产生一个std::string
  5. 有些查看类型推导结果的工具可能会产生无用或不准确的结果,因此理解C++类型推导的规则是必要的。

优先选用auto,而非显式类型声明

  1. 用auto声明的变量,其类型都推导自其初始化物,所以它们必须初始化。
  2. 由于auto使用了类型推导,就可以用它来表示只有编译器才掌握的类别,并且在C++14中,连lambda表达式的形参中都可以使用auto。
  3. 使用auto声明的、存储着一个闭包的变量和该闭包是同一类型,因此它所要求的内存量也和该闭包一样。但是使用std::function声明的、存储着一个闭包的变量是std::function的一个实例,所以无论=右边给定的签名如何,它都占有着固定尺寸的内存,而这个尺寸对于其存储的闭包并不一定够,如果是这样的话std::function的构造函数就会分配堆上的内存来存储该闭包,因此会比使用auto声明的变量使用更多的内存。
  4. 显式指定类型的可能导致你既不想要,也没想到的隐式类型转换,如果使用auto作为目标变量的类型,就完全没必要担心在用以声明变量的类型和它的初始化表达式的型别之间发生的不匹配。

auto推导类型不符合要求时,使用带显式类型的初始化物习惯用法

  1. “隐形”的代理类别可以导致auto根据初始化表达式推导出“错误的”类别,也就是代理类类别,而不是欲代理的类别,比如对std::vector执行operator[]操作得到的不是bool&,而是std::vector::reference这一代理类型。

  2. 要避免1中所提到的问题,我们只需要将代理类型进行一次强制类型转换即可得到我们想要的类型,比如auto x=static_cast<bool<(std::vectot<bool>[5])此时x就是bool类型了,这种用法就叫作带显示类型的初始化物习惯用法。