4.3.2 decltype的应用

在C++11中,使用decltype推导类型是非常常见的事情。比较典型的就是decltype与typdef/using的合用。在C++11的头文件中,我们常能看以下这样的代码:


using size_t=decltype(sizeof(0));

using ptrdiff_t=decltype((int)0-(int)0);

using nullptr_t=decltype(nullptr);


这里size_t以及ptrdiff_t还有nullptr_t(参见7.1节)都是由decltype推导出的类型。这种定义方式非常有意思。在一些常量、基本类型、运算符、操作符等都已经被定义好的情况下,类型可以按照规则被推导出。而使用using,就可以为这些类型取名。这就颠覆了之前类型拓展需要将扩展类型“映射”到基本类型的常规做法。

除此之外,decltype在某些场景下,可以极大地增加代码的可读性。比如代码清单4-18所示的例子。

代码清单4-18


include <vector>

using namespace std;

int main(){

vector<int> vec;

typedef decltype(vec.begin())vectype;

for(vectype i=vec.begin();i<vec.end();i++){

//做一些事情

}

for(decltype(vec)::iterator i=vec.begin();i<vec.end();i++){

//做一些事情

}

}

//编译选项:g++ -std=c++11 4-3-3.cpp


在代码清单4-18中,我们定义了vector的iterator的类型。这个类型还可以在main函数中重用。当我们遇到一些具有复杂类型的变量或表达式时,就可以利用decltype和typedef/using的组合来将其转化为一个简单的表达式,这样在以后的代码写作中可以提高可读性和可维护性。此外我们可以看到decltype(vec)::iterator这样的灵活用法,这看起来跟auto非常类似,也类似于是一种“占位符”式的替代。

在C++中,我们有时会遇到匿名的类型,而拥有了decltype这个利器之后,重用匿名类型也并非难事。我们可以看看代码清单4-19所示的例子。

代码清单4-19


enum class{K1,K2,K3}anon_e;//匿名的强类型枚举

union{

decltype(anon_e)key;

char*name;

}anon_u;//匿名的union联合体

struct{

int d;

decltype(anon_u)id;

}anon_s[100];//匿名的struct数组

int main(){

decltype(anon_s)as;

as[0].id.key=decltype(anon_e)::K1;//引用匿名强类型枚举中的值

}

//编译选项:g++ -std=c++11 4-3-4.cpp


这里我们使用了3种不同的匿名类型:匿名的强类型枚举anon_e(请参见5.1节)、匿名的联合体anon_u,以及匿名的结构体数组anon_s。可以看到,只要通过匿名类型的变量名anon_e、anon_u,以及anon_s,decltype可以推导其类型并且进行重用。这些都是以前C++代码所做不到的。事实上,在一些C代码中,匿名的结构体和联合体并不少见。不过匿名一般都有匿名理由,一般程序员都不希望匿名后的类型被重用。这里的decltype只是提供了一种语法上的可能。

进一步地,有了decltype,我们可以适当扩大模板泛型的能力。还是以代码清单4-9为例,如果我们稍微改变一下函数模板的接口,该模板将适用于更大的范围。我们来看看代码清单4-20中经过改进的例子。

代码清单4-20


//s的类型被声明为decltype(t1+t2)

template<typename T1,typename T2>

void Sum(T1&t1,T2&t2,decltype(t1+t2)&s){

s=t1+t2;

}

int main(){

int a=3;

long b=5;

float c=1.0f,d=2.3f;

long e;

float f;

Sum(a,b,e);//s的类型被推导为long

Sum(c,d,f);//s的类型被推导为float

}

//编译选项:g++ -std=c++11 4-3-5.cpp


相比于代码清单4-9的例子,代码清单4-20的Sum函数模板增加了类型为decltype(t1+t2)的s作为参数,而函数本身不返回任何值。这样一来,Sum的适用范围增加,因为其返回的类型不再是代码清单4-9中单一的double类型,而是根据t1+t2推导而来的类型。不过这里还是有一定的限制,我们可以看到返回值的类型必须一开始就被指定,程序员必须清楚Sum运算的结果使用什么样的类型来存储是合适的,这在一些泛型编程中依然不能满足要求。解决的方法是结合decltype与auto关键字,使用追踪返回类型的函数定义来使得编译器对函数返回值进行推导。我们会在4.4节中看到具体的细节(事实上,decltype一个最大的用途就是用在追踪返回类型的函数中)。

在代码清单4-20中模板定义虽然存在一些限制,但也基本是可以广泛使用的。但是不得不提的是,某些情况下,模板库的使用人员可能认为一些自然而简单的数据结构,比如数组,也是可以被模板类所包括的。不过很明显,如果t1和t2是两个数组,t1+t2不会是合法的表达式。为了避免不必要的误解,模板库的开发人员应该为这些特殊的情况提供其他的版本,如代码清单4-21所示。

代码清单4-21


template<typename T1,typename T2>

void Sum(T1&t1,T2&t2,decltype(t1+t2)&s){

s=t1+t2;

}

void Sum(int a[],int b[],int c[]){

//数组版本

}

int main(){

int a[5],b[5],c[5];

Sum(a,b,c);//选择数组版本

int d,e,f;

Sum(d,e,f);//选择模板的实例化版本

}

//编译选项:g++ -std=c++11 4-3-6.cpp


在代码清单4-21中,由于声明了数组版本Sum,编译器在编译Sum(a,b,c)的时候,会优先选择数组版本,而编译Sum(d,e,f)的时候,依然会对应到模板的实例化版本。这就能够保证Sum模板函数最大的可用性(不过这里的数组版本似乎做不了什么事情,因为数组长度丢失了)。

我们在实例化一些模板的时候,decltype也可以起到一些作用,我们可以看看代码清单4-22所示的例子。

代码清单4-22


include <map>

using namespace std;

int hash(char*);

map<char*,decltype(hash)>dict_key;//无法通过编译

map<char*,decltype(hash(nullptr))>dict_key1;

//编译选项:g++ -c-std=c++11 4-3-7.cpp


在代码清单4-22中,我们实例化了标准库中的map模板。因为该map是为了存储字符串以及与其对应哈希值的,因此我们可以通过decltype(hash(nullptr))来确定哈希值的类型。这样的定义非常直观,但是程序员必须要注意的是,decltype只能接受表达式做参数,像函数名做参数的表达式decltype(hash)是无法通过编译的。

事实上,decltype在C++11的标准库中也有一些应用,一些标准库的实现也会依赖于decltype的类型推导。一个典型的例子是基于decltype的模板类result_of,其作用是推导函数的返回类型。我们可以看一下应用的实例,如代码清单4-23所示。

代码清单4-23


include <type_traits>

using namespace std;

typedef double(*func)();

int main(){

result_of<func()>::type f;//由func()推导其结果类型

}

//编译选项:g++ -std=c++11 4-3-8.cpp


这里f的类型最终被推导为double,而result_of并没有真正调用func()这个函数,这一切都是因为底层的实现使用了decltype。result_of的一个可能的实现方式如下:


template<class>

struct result_of;

template<class F,class…ArgTypes>

struct result_of<F(ArgTypes…)>

{

typedef decltype(

std::declval<F>()(std::declval<ArgTypes>()…)

)type;

};


请读者忽略declval[1],这里标准库将decltype作用于函数调用上(使用了变长函数模板),并将函数调用表达式返回的类型typedef为一个名为type的类型。这样一来,代码清单4-23中的result_of<func()>::type就会被decltype推导为double。

[1]实际是STL中的一种语法技巧,更多的内容可以查阅一些在线文档,如http://en.cppreference.com/w/cpp/utility/declval。