首页 > 软件开发 > C++ > 正文
1.1.2 decltype关键字
2015-07-07 13:25:43     我来说两句      
收藏    我要投稿

1.?获知表达式的类型

上一节所讲的auto,用于通过一个表达式在编译时确定待定义的变量类型,auto所修饰的变量必须被初始化,编译器需要通过初始化来确定auto所代表的类型,即必须要定义变量。若仅希望得到类型,而不需要(或不能)定义变量的时候应该怎么办呢?

C++11新增了decltype关键字,用来在编译时推导出一个表达式的类型。它的语法格式如下:

decltype(exp)

其中,exp表示一个表达式(expression)。

从格式上来看,decltype很像sizeof——用来推导表达式类型大小的操作符。类似于sizeof,decltype的推导过程是在编译期完成的,并且不会真正计算表达式的值。

那么怎样使用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 = π     // *pp -> int *    , pp -> int * *

y和z的结果表明decltype可以根据表达式直接推导出它的类型本身。这个功能和上一节的auto很像,但又有所不同。auto只能根据变量的初始化表达式推导出变量应该具有的类型。若想要通过某个表达式得到类型,但不希望新变量和这个表达式具有同样的值,此时auto就显得不适用了。

j的结果表明decltype通过表达式得到的类型,可以保留住表达式的引用及const限定符。实际上,对于一般的标记符表达式(id-expression),decltype将精确地推导出表达式定义本身的类型,不会像auto那样在某些情况下舍弃掉引用和cv限定符。

p、pi的结果表明decltype可以像auto一样,加上引用和指针,以及cv限定符。

pp的推导则表明,当表达式是一个指针的时候,decltype仍然推导出表达式的实际类型(指针类型),之后结合pp定义时的指针标记,得到的pp是一个二维指针类型。这也是和auto推导不同的一点。

对于decltype和引用(&)结合的推导结果,与C++11中新增的引用折叠规则(Reference Collapsing)有关,因此,留到后面的2.1节右值引用(Rvalue Reference)时再详细讲解。

关于p、pi、pp的推导,有个很有意思的地方。像MicrosoftVisual Studio这样的IDE,可以在运行时观察每个变量的类型。我们可以看到p的显示是这样的:


 

这其实是C/C++的一个违反常理的地方:指针(*)、引用(&)属于说明符(declarators),在定义的时候,是和变量名,而不是类型标识符(type-specif?iers)相结合的。
因此,“const decltype(z)*p”推导出来的其实是*p的类型(const int),然后再进一步运算出p的类型。

2.decltype的推导规则

从上面一节内容来看,decltype的使用是比较简单的。但在简单的使用方法之后,也隐藏了不少细节。

我们先来看看decltype(exp)的推导规则:

推导规则1,exp是标识符、类访问表达式,decltype(exp)和exp的类型一致。

推导规则2,exp是函数调用,decltype(exp)和返回值的类型一致。

推导规则3,其他情况,若exp是一个左值,则decltype(exp)是exp类型的左值引用,否则和exp类型一致。

只看上面的推导规则,很难理解decltype(exp)到底是一个什么类型。为了更好地讲解这些规则的适用场景,下面根据上面的规则分3种情况依次讨论:

1)标识符表达式和类访问表达式。

2)函数调用(非标识符表达式,也非类访问表达式)。

3)带括号的表达式和加法运算表达式(其他情况)。

(1)标识符表达式和类访问表达式

先看第一种情况,代码清单1-3是一组简单的例子。

代码清单1-3 decltype作用于标识符和类访问表达式示例

class Foo

{
public:
   static const int Number = 0;
   int x;
};

int n = 0;
volatile const int & x = n;

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

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

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

变量a、b、c保留了表达式的所有属性(cv、引用)。这里的结果是很简单的,按照推导规则1,对于标识符表达式而言,decltype的推导结果就和这个变量的类型定义一致。

d是一个类访问表达式,因此也符合推导规则1。

(2)函数调用

接下来,考虑第二种情况:如果表达式是一个函数调用(不符合推导规则1),结果会如何呢?

请看代码清单1-4所示的示例。

代码清单1-4 decltype作用于函数调用的示例

int& func_int_r(void);   // 左值(lvalue,可简单理解为可寻址值)
int&& func_int_rr(void);   // x值(xvalue,右值引用本身是一个xvalue)
int func_int(void);   // 纯右值(prvalue,将在后面的章节中讲解)

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

const Foo func_cfoo(void);  // 纯右值

// 下面是测试语句
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_cfoo())    ff = Foo(); // ff -> const Foo

可以看到,按照推导规则2,decltype的结果和函数的返回值类型保持一致。

这里需要注意的是,c2是int而不是const int。这是因为函数返回的int是一个纯右值(prvalue)。对于纯右值而言,只有类类型可以携带cv限定符,此外则一般忽略掉cv限定。

如果在gcc下编译上面的代码,会得到一个警告信息如下:

warning: type qualif?iers ignored on function return type
[-Wignored-qualif?iers]
 cint func_cint(void);

因此,decltype推导出来的c2是一个int。
作为对比,可以看到decltype根据func_cfoo()推导出来的ff的类型是const Foo。

(3)带括号的表达式和加法运算表达式

最后,来看看第三种情况:

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

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

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

a和b的结果:仅仅多加了一对括号,它们得到的类型却是不同的。

a的结果是很直接的,根据推导规则1,a的类型就是foo.x的定义类型。

b的结果并不适用于推导规则1和2。根据foo.x是一个左值,可知括号表达式也是一个左值。因此可以按照推导规则3,知道decltype的结果将是一个左值引用。

foo的定义是const Foo,所以foo.x是一个const int类型左值,因此decltype的推导结果是const int &。

同样,n+m返回一个右值,按照推导规则3,decltype的结果为int。

最后,n+=m返回一个左值,按照推导规则3,decltype的结果为int &。

3.?decltype的实际应用

decltype的应用多出现在泛型编程中。考虑代码清单1-5的场景。

代码清单1-5 泛型类型定义可能存在问题的示例

#include <vector>

template <class ContainerT>
class Foo
{
   typename ContainerT::iterator it_; // 类型定义可能有问题
public:
   void 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;
}

单独看类Foo中的it_成员定义,很难看出会有什么错误,但在使用时,若上下文要求传入一个const容器类型,编译器马上会弹出一大堆错误信息。

原因就在于,ContainerT::iterator并不能包括所有的迭代器类型,当ContainerT是一个const类型时,应当使用const_iterator。

要想解决这个问题,在C++98/03下只能想办法把const类型的容器用模板特化单独处理,比如增加一个像下面这样的模板特化:

template <class ContainerT>
class Foo<const ContainerT>
{
   typename ContainerT::const_iterator it_;

public:
   void func(const ContainerT& container)
   {
      it_ = container.begin();
   }

   // ...
};

这实在不能说是一个好的解决办法。若const类型的特化只是为了配合迭代器的类型限制,Foo的其他代码也不得不重新写一次。

有了decltype以后,就可以直接这样写:

template <class ContainerT>
class Foo
{
   decltype(ContainerT().begin()) it_;

public:
   void func(ContainerT& container)
   {
      it_ = container.begin();
   }

   // ...
};

是不是舒服很多了?

decltype也经常用在通过变量表达式抽取变量类型上,如下面的这种用法:

vector<int> v;
// ...
decltype(v)::value_type i = 0;

在冗长的代码中,人们往往只会关心变量本身,而并不关心它的具体类型。比如在上例中,只要知道v是一个容器就够了(可以提取value_type),后面的所有算法内容只需要出现v,而不需要出现像vector<int>这种精确的类型名称。这对理解一些变量类型复杂但操作统一的代码片段有很大好处。

实际上,标准库中有些类型都是通过decltype来定义的:

typedef decltype(nullptr)nullptr_t;// 通过编译器关键字nullptr定义类型nullptr_t
typedef decltype(sizeof(0)) size_t;

这种定义方法的好处是,从类型的定义过程上就可以看出来这个类型的含义。

点击复制链接 与好友分享!回本站首页
您对本文章有什么意见或着疑问吗?请到论坛讨论您的关注和建议是我们前行的参考和动力  
上一篇:1.1.1 auto类型推导
下一篇:1.1.3 返回类型后置语法——auto和decltype的结合使用
相关文章
图文推荐
排行
热门
文章
下载
读书

关于我们 | 联系我们 | 广告服务 | 投资合作 | 版权申明 | 在线帮助 | 网站地图 | 作品发布 | Vip技术培训 | 举报中心

版权所有: 红黑联盟--致力于做实用的IT技术学习网站