首页 > 技术文章 > C11简洁之道:类型推导

ChinaHook 2017-10-10 22:29 原文

1、  概述

  C++11里面引入了auto和decltype关键字来实现类型推导,通过这两个关键字不仅能方便的获取复杂的类型,还能简化书写,提高编码效率。

2、  auto

2.1 auto关键字的新定义

  auto关键字并不是一个全新的关键字,早在旧标准中就已经有定义:“具有自动储存期的局部变量”,不过用处并不大,如下:

auto int i = 0;   //c++98/03,可以默认写成int i = 0;

static int j = 0;

  上述代码中,auto int是旧标准中的用法,与之相对的是static int,代表了静态类型的定义方法,我们很少这样使用auto,因为非static的局部变量本身就具有自动存储期。

  所以,c++11中考虑到之前的auto使用较少,不再表示存储类型指示符(如static,mutable等),而是改成了类型指示符,用来提示编译器对此类型的变量做类型的自动推导。

  我们来看一组例子:

auto x = 5;            //OK,x:int
auto pi = new auto(1);     //OK,pi:int *
const auto *v = &x, u = 6;   //OK,v:const int *, u:const int
static auto y = 0.0;          //OK,y:double
//auto int r;                 //error,auto不再表示存储类型指示符
//auto s;                     //error,无法推导出类型

  x被自动推导为int,并被初始化为5;

  pi被推导为int *,同时说明auto还支持new的类型推导;

  &x类型为int *,所以const auto *说明auto是int,所以v为const int *;

  因为v为int *,所以推导auto为ing,u也应该为int,这里需要注意的是,u的初始化必须和前面推导的auto类型相同,不然会出现二义性,否则编译器通不过;

  y被推导为double;

  auto已经不再是存储类型指示符,所以,r会提示错误;

  s没类型进行推导,所以会报错没有初始化。

  由列子可以得出以下结论:

  auto并不能代表一个实际的类型,只是一个类型申明的占位符;

  auto申明的变量必须马上初始化,让编译器在编译期间推导出实际类型,在编译的时候用实际的类型替换掉类型占位符auto。

2.2 auto推导规则

  auto可以同指针引用结合起来使用,还可以带上cv限定符(const,volatile的统称)。下面我们再来看一组列子:

int x = 0;
auto *a = &x; //a -> int*,auto->int
auto b = &x;  //b -> int*,auto->int*
auto &c = x;  //c -> int&,auto->int
auto d = c;   //d -> int ,auto->int

const auto e = x; //e->const int
auto f = e;       //f->int

const auto &g = x; //g->const int &
auto &h = g;       //f->const int &

  上述例子的推导结果如代码,从例子我们可以得出以下结论:

  auto可以自动推导指针类型;

  当不申明为指针或者引用时,auto的推导结果和初始化表达式抛弃引用和cv限定符后的类型一致;

  当申明为指针或者引用时,auto推导的结果将保持初始化表达式的cv属性。

2.3 auto的限制

  auto申明的时候必须初始化,那么auto肯定是不能作为函数参数的。

  void func(auto a = 2){}   //error:不能用于函数参数

  auto不能用于非静态成员变量。

struct Foo
{
  auto var1 = 0;              // error
  static const auto var2 = 1; //OK,var2->static const int
}

  auto无法定义数组。

int arr[10] = {0};
auto aa = arr;       //OK,aa->int *
auto arr2[10] = {0}; //error无法通过编译

  auto无法推导出模版参数。

std::vector<int> vec1;         //OK
std::vector<auto> vec2 = vec1; //error 无法通过编译

  在Foo中,auto仅能推导static const的整型或者枚举成员(因为其他静态类型在c++标准中无法就地初始化),c++11中可以接受非静态成员变量的就地初始化,但不支持auto类型非静态成员变量的初始化。

2.4 auto使用场景

  auto在我看来,最主要的是可以简洁代码。类型推导虽然说可以作自动推导,但是在真正写代码的时候,还要考虑可读性,auto并不能代码更多的好处。

  如果在c++11之前,我们定义一个map,在遍历的时候,通常需要这么写:

void func()
{
  map<int, int> test_map;
  map<int, int>::iterator it = test_map.begin();

  for(it; it != test_map.end(); ++it)
  {
      //do something
  }
}

  那么我们在使用auto之后,代码就会很简单了,根据map.begin()就可以推导出类型。

void func2()
{
  map<int, int> test_map;

  for(auto it = test_map.begin(); it != test_map.end(); ++it)
  {
      //do something
  }
}

  是不是感觉简洁多了。更简洁的还在后面,我们在一个unordered_multimap中查找一个范围:

void func3()
{
  unordered_multimap<int, int> test_map;
  pair<unordered_multimap<int, int>::iterator, unordered_multimap<int, int>::iterator> range = test_map.equal_range(5);
}

  很明显,这个euqal_range返回的类型显得太繁琐,而实际上可能并不在乎这里的具体类型,这时就可以通过auto来简化书写。使用auto之后:

void func4()
{
  unordered_multimap<int, int> test_map;
  auto range = test_map.equal_range(5);
}

  如果在某些情况不知道返回值类型,我们可以通过auto来做推导,然后统一处理。

class Foo
{
public:
  static int get()
  {
    return 0;
  }
};

class Bar
{
public:
  static const char *get()
  {
     return "0";
  }
};

template <class A>
void func5()
{
  auto val = A::get();
  // do something
}

void func6()
{

  func5<Foo>();
  func5<Bar>();

  return; }

  假如我们定义一个泛型函数func5,对具有静态方法get的类型A得到的结果做统一的后续处理,如果不使用auto,那么就必须再增加一个模板参数,并在外部手动指定get的返回值类型。

  auto是一个很强大的工具,但是如果不加选择的随意使用auto会导致代码可读性和可维护性严重下降,因此,在使用的时候,一定要权衡好使用的“度”,那么带来的价值会非常大。

3、  decltype

3.1 基本定义

  auto的申明必须要初始化才能确定auto代表的类型,那如果我们既需要得到类型,又不定义变量的时候怎么办呢?C++11提供了decltype来解决这个问题,它的定义如下:

decltype(exp)   //exp表示一个表达式。

  decltype有点像sizeof,不过sizeof是计算表达式类型大小的标识符。和sizeof一样,decltype也是在编译时期推导类型的,并且不会真正计算表达式的值。我们来看一组例子:

int x = 0;
decltype(x) y = 1;                       //y->int
decltype(x+y) z = 0;                     //z->int   

const int & i = x;
decltype(i) j = y;                       //j->const int & 
const decltype(z) *p = &z;               //*p->const int, p->const int *
decltype(z) *pi = &z;                    //*pi->int, pi->int *
decltype(pi) * pp = &pi;                 // *pp->int *, pp->int **    

  decltype和auto一样,可以加上引用指针,以及cv限定符进行推导。

3.2 推导规则

  网上各种版本的规则众多,下面是我简单整理的一些规则:

  exp是标识符、类访问表达式,decltype(exp)和exp的类型一致;

  exp是函数调用,decltype(exp)和函数返回值一致;

  exp是一个左值,则decltype(exp)是exp的一个左值引用,否则和exp的类型一致。

3.2.1 标识符表达式和类访问表达式

class Foo
{
public:
    static const int miNum = 0;
    int ix;
};

int n = 0;

volatile const int &x = n;
decltype(n) a = n;              //a->int
decltype(x) b = n;              //b->const valatile int &

decltype(Foo::miNum) c = 0;     //c->const int

Foo foo;
decltype(foo.ix) d = 0;         //d->int,类访问表达式

  变量abc保留了表达式的所有属性(cv、引用),对于表达式,decltype的推导和表达式一致,而d是一个类访问表达式,因此也和表达式类型一致。

3.2.2 函数调用

int &func_int_r(void);                      //左值,lvalue,简单理解为可寻址值
int &&func_int_rr(void);                    //x值,xvalue,右值引用本身是一个xvalue
int func_int(void);                         //纯右值,pvalue

const int &func_cint_r(void);               //左值
const int && func_cint_rr(void);            //x值
const int func_cint(void);                  //纯右值

cont Foo func_foo(void);                    //纯右值

//下面是测试语句

int x = 0;

decltype(func_int_r())  al = x;         //al->int&
decltype(func_int_rr()) bl = 0;         //bl->int &&
decltype(func_int())    cl = 0;         //cl->int

decltype(func_cint_r())  a2 = x;        //a2->const int &
decltype(func_cint_rr()) b2 = 0;        //b2->const int &&
decltype(func_cint)      c2 = 0;        //c2->int
decltype(func_foo()) ff = Foo();        //ff->const Foo

  这里需要注意的是,C2的推导是int而不是const int,这是因为函数的返回值int是个纯右值,对于纯右值而言,只有类类型可以携带cv限定符,除此之外一般忽略掉。所以func_foo推导出来的ff是const Foo。

3.2.3 带括号的表达式和加法运算表达式

struct Foo{ int x;};
const Foo foo = Foo();

decltype(foo.x)   a = 0;   //a->int
decltype((foo.x)) b = a;   //const int &

int n = 0, m = 0;
decltype(n+m)  c = 0;      //c->int
decltype(n+=m) d = c;      //d->int &

  这里需要注意的是,b的推导,括号表达式中的foo.x是一个左值,所以decltype的结果是一个左值引用,foo的定义是const Foo,所以foo.x是一个const int类型,所以b是一个const int &;

  n+m返回的是一个右值,所以结果是int,n+=m返回一个左值,所以推导出d是int &。

3.3 decltype实际应用

  decltype多出现在泛型变成中,我们来看一个例子,假如我们编写一个泛型类:

template<class ContainerT>
class Foo
{
    typename ContainerT::iterator it; //类型的定义可能有问题
publicvoid func(ContainerT &container)
    {
        it = container.begin();
    }
}

int main(void)
{
    typedef const std::vector<int> container_t;
    
    container_t arr;
    
    Foo<container_t> foo;
    foo.func(arr);

    return 0;
}

  问题很明显,当我们传入一个const容器的时候,我们的定义的iterator不适用,编译器会报错,所以当传入const容器的时候,我们应该使用const_iterator。

  如果在C++98/03要解决这样的问题,我们必须增加一个泛型类:

template<class ContainerT>
class Foo<const ContainerT >
{
    typename ContainerT::const_iterator it; //类型的定义可能有问题
publicvoid func(const ContainerT &container)
    {
        it = container.begin();
    }
}

  这样虽然说可以解决问题,但是太麻烦,Foo的其他代码也不得不重新写一次,如果我们用decltype来解决这个问题:

template<class ContainerT>
class Foo
{
    decltype(ContainerT().begin()) it; //类型的定义可能有问题
publicvoid func(ContainerT &container)
    {
        it = container.begin();
    }
}

  decltype也经常用在通过变量表达式抽取变量类型上:

vector<int> v;

decltype(v)::value_type I = 0;

  冗长的代码中,我们只需要关心变量本身,不需要关心它的具体类型,如例子,我们只需要之道v是一个容器,可以提取value_type就OK,而不是到处都需要出现vector<int>这种精确的类型名称。

4、  auto和decltype混编

  在泛型编程中,通过auto和decltype的混编来提升灵活性。通过参数的运算来获得返回值类型。

template<typename R, typename T, typename U>
R add(T t, U u)
{
    return t + u;
}

int a = 1;
float b = 2.0;

auto c = add<decltype(a + b)>(a, b);

  我们不关心a+b类型是什么,通过decltype(a+b)来推导返回值类型。我们还可以通过add函数的定义来获得返回值类型。

template<typename T, typename U>
decltype(t+u) add(T t, U u) //error :t、u尚未定义
{
  return t + u;
}

  c++的返回值是前置语法,在返回值的时候参数变量都还不存在,所以这样是编译不通过的,我们可以通过构造函数来进行推导:

template<typename T, typename U>
decltype(T() + U()) add(T t, U u)
{
    return t + u;
}

  但是这样也有一个问题,T、U类型可能是没有无参构造函数,我们可以进行改善:

template<typename T, typename U>
decltype((*(T*)0) + (*(U*)0)) add(T t, U u)
{
    return t + u;
}

  这样可以成功的使用decltype来完成返回值的推导,但是代码可读性比较差,并增加使用难度。

  c++11增加了返回类型后置语法,讲decltype和auto结合起来完成返回值类型的推导。我们再来改善上面的add函数:

template<typename T, typename U>
auto add(T t, U u) -> decltype(t + u)
{
    return t + u;
}

  我们再来看一个例子:

int &foo(int &i);
float foo(float &f);
 
template<typename T>
auto func(T &val) -> decltype(foo(val))
{
    return foo(val);
}

  使用decltype结合返回值后置语法很容易推导出foo(val)可能出现的返回值,并用到func上。返回值类型后置语法,是为了解决函数返回值类型依赖与参数而导致难以确定返回值类型的问题,可以很清晰的描述返回值类型,而不是c++98/03那样晦涩难懂的语法来解决问题。

推荐阅读