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。