2_decltype关键字(深入应用C++11代码优化与工程级应用)

1. decltype介绍

  在上一篇中我们简单介绍了auto, auto就是一个"站位符",在编译时编译器根据初始化的值的类型来推导当前定义的变量的类型. 所以使用auto定义变量时一定要对该变量进行初始化. 如果我们希望得到类型, 而不给新定义的变量进行初始化时要怎么做呢?

  decltype关键字, 用来在编译时推导出一个表达式的类型.

decltype(exp)

  其中, exp是一个表达式(expression). 但是这个表达式的值肯定是可推导的.

  decltype简单使用:

int func(){
    cout<<"----func----"<<endl;
    return 0;
}

int x = 0;              // int
decltype (x) y = 1;     // y被推导为int     decltype推导结果为int
decltype (x+y) z = 0;   // z被推导为int     decltype推导结果为int

const int & i = x;      // int const &
decltype (i) j = y;     // j被推导为 int const &    decltype推导结果为int const &

const decltype (z) * p = &z;    // p被推导为int const *     decltype推导为int
decltype (z) * pi = &z;         // pi被推导为int *          decltype推导为int
decltype (pi) * pp = &pi;       // pp被推导为int **         decltype推导为int*

int *** ppp = nullptr;  // int***
decltype(ppp) pppp;     // pppp被推导为int ***  decltype推导为int ***
decltype(ppp)* ppppp;   // ppppp被推导为int **** decltype推导为int ***

int r = 10;
int &rr = r;
decltype (rr) dr = r;       // dr被推导为int &    decltype推导为int &
decltype (rr) & drr = r;    // drr被推导为int &   decltype推导为int &
decltype (rr) && drrr = r;  // drrr被推导为int &  decltype推导为int &

int && rv = 10;             // int &&
decltype(rv) rrv = 10;      // rrv被推导为int &&    decltype推导为int &&
decltype(rv) & rrrv = r;    // rrrv被推导为int &    decltype推导为int &&,但由于引用折叠(由于左值引用&具有传染性),所以& rrrv是左值,rrrv也就被传染为int &.
decltype(rv) && rrrrv = 10; // rrrrv被推导为int &&    decltype推导为int &&,但由于引用折叠,int && &&就是int &&.

decltype((x)) k = x;        // k被推导为int &

decltype (func()) f;        // f被推导为int func()函数不会被执行

decltype (k+r) u;           // u被推导为int     k+r返回一个右值

decltype (k+=r) uu = r;     // uu被推导为int &  K+=r返回一个左值

  根据上面示例可以看出使用decltype推导出的类型为exp表达式的真正类型,并不会抛弃cv限定符.需要注意的是,变量的类型最终是decltype推导的类型与额外的限定符的集合. 在左值引用和右值引用的时候最为明显,最终的类型会根据引用折叠的规则得到.

2. decltype推导规则

decltype(exp)的推导规则:

  • 规则1, exp是标识符\访问表达式是, decltype(exp)推导的类型和exp的类型一致.
  • 规则2, exp是函数调用, ecltype(exp)推导的类型和返回值的类型一致.注意:该函数并不会被执行.
  • 规则3, exp是一个左值(引用), 或者带括号的表达式, 则decltype(exp)是exp类型的左值引用, 否则和exp类型一致.

规则1举例:

int n = 0;
volatile const int & x = n;     // int const volatile&
decltype (n) a = n;             // a被推导为int
decltype (x) b = n;             // b被推导为int const volatile&
decltype (F::number) c = 0;     // c被推导为int const
F f;
decltype (f.x) d = 0;           // d被推导为int

  对于a, b, c三个变量来说,它们推导的结果是exp标识符(n, x, F::number都是标识符)的所有属性(cv\引用). d的类型是exp类成员访问表达式(f.x 是类成员的访问表达式)的属性.

规则2举例:

int x = 0;
decltype (func_int_r()) a1 = x;     // a1被推导为int&
decltype (func_int_rr()) b1 = 0;    // b1被推导为int&&
decltype (func_int()) c1 = 0;       // c1被推导为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_cf()) ff = F();      // ff被推导为F const

decltype (func_int_p()) a3 = &x;     // a3被推导为int*
decltype (func_int_pp()) b3 = &a3;   // b3被推导为int**
decltype (func_cint_p()) a4 = &x;    // a3被推导为int const*
decltype (func_cint_pp()) b4 = &a4;  // b3被推导为int const**

  可以看出,根据规则2, 推导的类型和函数返回值的类型一致. 这里要注意的是c2推导的结果是int, 而不是const int. 这是因为函数返回的int是一个纯右值(prvalue). 对纯右值而言, 只有类类型可以携带cv限定符,除此之外一般都忽略cv限定符.

  在gcc编译器下对返回值为const int的函数会有警告 warning: 'const' type qualifier on return type has no effect.

规则3举例:

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

decltype (foo.x) a = 0;     // a被推导为int
decltype ((foo.x)) b = a;   // b被推导为int const&

int n = 0;
int m = 0;
decltype (n+m) c = 0;       // c被推导为int
decltype (n+=m) d = c;      // d被推导为int&

  a和b的推导结果:仅仅是多加了一对括号,他们得到的类型却不相同.

  • a的结果很直接, 根据规则1进行推导得到的结果. 虽然foo是const类型的, 但最终推导时使用的类型并不包括类的限定符, 只包含类成员的类型.
  • b的结果并不准寻规则1和规则2. 根据foo.x是一个左值, 可知括号表达式也是一个左值表达式(可以有具名的地址).因此可以按照推导规则3, 因此b被推导为左值引用. 由于是左值引用, 那么推导的类型要与实际的成员的类型是一致的. 因为foo是const, 因此x也会被此限定符限制. 所以, foo.x的左值引用类型为const int &.
  • n+m返回的是一个右值, 按照规则三推导, 返回表达式的类型int.
  • n+=m返回的是一个左值, 按照规则三, 返回的是表达式的类型的左值引用,也就是int &.

  上面我们看到了括号表达式是左值,+=操作也是左值.那我们如何区分是不是左值表达式呢?

  后面会讨论左值,右值,消亡值,纯右值等属性.

  下面例子只能简单说明+=表达式是个左值表达式.

int x = 0;
int y = 0;
x+y = 1;        // error: expression is not assignable
//(x) = 1;      // 这个无视
(x+=y) = 1;

3. 返回类型后置语法

template <typename T, typename U>
decltype (t+u) add(T t, U u){   // error: use of undeclared identifier 't' error: use of undeclared identifier 'u'
    return t+u;
}

  直接像上面这样写是无法编译通过的. 因为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;
}

  即使正确推导出了返回值类型, 有没有发现根本无法理解为什么这样写, 完全是从网上或书上照搬照抄的. 下面将使用auto和decltype结合使用推导返回值类型, 其推导结果一目了然, 更容易理解.

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

posted on 2022-07-26 19:30  dogpi  阅读(81)  评论(1编辑  收藏  举报

导航