主要关于类型推断的一些条款。

Item 1:Understand template type deduction

在模板的使用过程中,我们需要区分两个类型TParamtypeT是基本类型,paramtype是包含CV修饰后的结果。

template<typename T> 
void f(const T& param); 

int expr = 0;
f(expr);

解释一下param typeparam最后的类型

下面就围绕T, expr, param type展开讨论:

(1)Case 1:ParamType is a Reference or Pointer, but not a Universal Reference

param type引用或者指针类型时,它的推断遵从两个原则

  1. 忽略exprparam type包含的部分得到T
  2. Tparam type配对的到param
template<typename T> void f(T& param);       // param is a reference

int x = 27;             // x is an int 
const int cx = x;       // cx is a const int 
const int& rx = x;      // rx is a reference to x as a const int

f(x);     // T is int, param's type is int&
f(cx);    // T is const int,                     
        // param's type is const int&
f(rx);    // T is const int,                     
        // param's type is const int&

//////////////////////////////
template<typename T> void f(const T& param);  // param is now a ref-to-const
int x = 27;              // as before 
const int cx = x;        // as before 
const int& rx = x;       // as before
f(x);     // T is int, param's type is const int&
f(cx);     // T is int, param's type is const int&
f(rx);     // T is int, param's type is const int&

////////////////////////////////////
//指针也适用这个原则
template<typename T> void f(T* param);        
// param is now a pointer
int x = 27;                 // as before 
const int *px = &x;         // px is a ptr to x as a const int
f(&x);    // T is int, param's type is int*
f(px);  // T is const int,               
        // param's type is const int*

(2)Case 2: ParamType is a Universal Reference

当考虑全局引用(universal reference, T&&)的时候,情况会变得有些不一样。他遵循两个原则:

  1. 如果expr是左值,T和param都被推为左值引用(不管有没有统统加上引用符)
  2. 如果是右值,T不变,param加上&&。
template<typename T> void f(T&& param);       // param is now a universal reference
int x = 27;              // as before 
const int cx = x;        // as before 
const int& rx = x;       // as before
f(x);                   // x is lvalue, so T is int&, 
                        // param's type is also int&
f(cx);                  // cx is lvalue, so T is const int&,   
                        // param's type is also const int&
f(rx);                  // rx is lvalue, so T is const int&,   
                        // param's type is also const int&
f(27);                  // 27 is rvalue, so T is int,           
                        // param's type is therefore int&&

(3)case3: ParamType is Neither a Pointer nor a Reference

这就是最基本的值传递。意味着,param复制了一个传入的参数。

template<typename T> void 
f(T param);         // param is now passed by value
  • 如果expr是引用,忽略引用的部分
  • 如果忽略引用后,exprconst类型或volatile类型,忽略。
int x = 27;          // as before 
const int cx = x;    // as before 
const int& rx = x;   // as before
f(x);                // T's and param's types are both int
f(cx);               // T's and param's types are again both int
f(rx);               // T's and param's types are still both int

注意:只是忽略引用,指针还是不变

template<typename T> void f(T param);         
// param is still passed by value
const char* const ptr ="Fun with pointers";  
// ptr is const pointer to const object  
f(ptr);                  
// pass arg of type const char * const
const int theAnswer = 42;

auto x = theAnswer;
auto y = &theAnswer; // const int* 取引用相当于转成了指针

首先要澄清一个误区:数组类型和指针类型是完全不一样的(虽然他们在使用时可以混用)。由于C语言老祖宗的继承关系,C++依然保留了这些特性,导致很多人误以为数组和指针参数是一样的。

在传递过程中,数组会退化为它第一个元素的指针。注意,这里是退化

template<typename T> void f(T param);      // template with by-value parameter
const char name[] = "J. P. Briggs";  // name's type is const char[13]

f(name); // name is array, but T deduced as const char*

这种退化导致有用的信息(数组长度)丢失。然而声明引用,可以使得模板推断保留成数组的形式

template<typename T>
void f(T& param);
f(name);  //deduce to const char[13]

根据原则:忽略paramtype包含的部分T,T加上包含的部分得到param。在这个例子中T被推导为const char[13]param则被推导为const char(&)[13]

如果改为T&&,根据原则:统统推导为左值引用。则Tparam都被推为const char(&)[13]

利用这一特性,我们在对模板函数声明为一个指向数组的引用使得我们可以在模板函数中推导出数组的大小:

template<typename T, std::size_t N>                
constexpr std::size_t arraySize(T (&)[N]) noexcept 
{   
    return N; 
}

关键词constexpr能够让结果在编译时有效,从而实现以下的效果:

int keyVals[] = { 1, 3, 7, 9, 11, 22, 35 };      // keyVals has 7 elements
int mappedVals[arraySize(keyVals)];

讲完数组,再来讲讲函数。在C++中不止是数组会退化为指针,函数类型也会退化为一个函数指针,我们对于数组的全部讨论都可以应用到函数来:

void someFunc(int, double);  // someFunc is a function;
                             // type is void(int, double)
template<typename T> void f1(T param);     // in f1, param passed by value
template<typename T> void f2(T& param);    // in f2, param passed by ref
f1(someFunc);               // param deduced as ptr-to-func;
                            // type is void (*)(int, double)
f2(someFunc);               // param deduced as ref-to-func;
                            // type is void (&)(int, double)

Item 2:Understand auto type deduction.

auto推断和模板推断具有清晰的映射关系,也分为3+1种情况:

(1) 参数类型为指针或引用,但不是universal reference

int x=7;
auto& rx = x; //rx is int &
int& z = x;
const auto& rx = z; // rx is const int&

(2)参数类型为universal reference

auto x = 7;
const int cx = 7;
auto &&uref1 = x;  // uref1的类型为int &
auto &&uref2 = cx; // uref2的类型为const int &
auto &&uref3 = 27; // uref3的类型为int &&

(3)参数类型为pass-by-value

auto x = 7; // x is int
const int& y=10;
auto rx = y; // rx is int

Tips: 我们在使用for-auto语句时,以下两种情况完全不同,一个可以改变原有的值一个不能。原因就是auto&x推断为 int&

for(auto x:nums)
for(auto& x:nums)

(4) 数组和函数

const char name[] = [...]           // name's type is const char[13]  "R. N. Briggs";
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和模板推断不一样的地方:

在C++中auto初始化有四种方式:

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

前两个会被推导为int类型,后两个则是std::initial izer_list。这是因为当auto遇到花括号时会做自动转化,因此,以下情况是不允许的:

auto x5 = { 1, 2, 3.0 };   // error!

与模板推断不同的地方如下:

auto x = { 11, 23, 9 };   // x's type is std::initializer_list<int>
template<typename T>     
f({ 11, 23, 9 });         // error! can't deduce type for T

如果我们改一下:

template<typename T> void f(std::initializer_list<T> initList);
f({ 11, 23, 9 });         // T deduced as int, and initList's type is std::initializer_list<int>

值得注意的是C++14允许auto用于函数返回值并会被推导(参见Item3),而且C++14的lambda函数也允许在形参中使用auto。在表面上使用的是auto但是实际上是模板类型推导的那一套规则在工作,所以说下面这样的代码不会通过编译:

auto createInitList()
{
    return {1,2,3};     //错误!
}

Item 3:Understand Decltype.

decltype不像是auto和模板推断那样有着很多奇奇怪怪的限制。它,简单直观,是什么类型就一定返回什么类型。举一些例子:

const int i = 0;           // decltype(i) is const int
bool f(const Widget& w);   // decltype(w) is const Widget& 
                            // decltype(f) is bool(const Widget&)
Widget w;                  // decltype(w) is Widget
if (f(w)) …                // decltype(f(w)) is bool
vector<int> v;             // decltype(v) is vector<int> 
if (v[0] == 0)               // decltype(v[0]) is int&

在C++11中decltype结合auto还可以完成函数返回值的类型推导

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

到了C++14的时候就可以省略掉后面的-> decltype(c[i])了,变成下面的样子。

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

更高级的可以改为如下形式。它的作用原理是:auto声明表示“我要进行推断了”,而decltype则表示“auto你听我说,必须按照我decltype的方法推断(原封不动)”

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

通过decltype保证返回变量的本来类型这一特性,保证不丢失CV限制符,和引用等,因此在C++14中可以通过decltypeauto来声明变量,保证变量的类型和赋值的类型一模一样

const int& cw = 10;
auto autoia = cw;             //推导出的类型是int,引用和CV限制符都会忽略
decltype(auto) deautoia = cw; //const int& 保证和cw的类型一模一样

上面的方案通过decltypeauto让返回值的类型变的完美,但是如果用户传入一个const的容器,将会导致编译出错。因为AccessContainer的参数类型是非常量引用,为了让他可以接收常量和非常量。

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

这带来的另外一个问题就是,c[i],返回的是常量引用,无法修改。好在C++11中引入了右值引用,它可以接收左值,右值还有带const的。

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

由于传入后,会把c的原本的形式给整没,也就是说:如果用户传入的是一个右值,通过移动语义传递给了AccessContainer的参数c,c变成了一个左值,如果在AccessContainer中需要把c再次传递给其他的函数的话就不能再次利用右值的移动语义了,带来了不必要的拷贝开销。那么就需要用到完美转发

template<typename Container,typename Index>
decltype(auto) AccessContainer(Container&& c,Index i) {
    return std::forward<Container>(c)[i];
}

Item 4:Know how to view deduced types.

这一节主要针对实际问题做讨论,做一下简化,只讨论可能用得到的。

查看类型推导最简单的方法就是在IDE里面,把鼠标移到变量上查看。大部分情况下是很好用的,但不能适用于很复杂的情况。

如果要在运行时推断,有两种方法:

(1)typeid的应用。简单明了,但忽略了CV限定符和引用

#include <typeinfo>
const int& m = 10;
std::cout << typeid(decltype(m)).name() << std::endl; //输出int

(2)boost库函数。能准确提取结果,但需要额外安装boost库。windows安装方法

void f(const T& param) {
    using std::cout;
    using boost::typeindex::type_id_with_cvr;

    cout << "param = "
        << type_id_with_cvr<T>().pretty_name()
        << '\n';

    cout << "param = "
        << type_id_with_cvr<decltype(param)>().pretty_name()
        << '\n';
}
const int& m = 10;
f(m);
//output: 
param = int
param = int const & __ptr64