3.了解decltype技术

前言

 
decltype是一项奇特的技术,它能够告诉你某个名称或表达式的类型,尽管有时候返回的结果令你抓耳挠腮。


decltype的使用

 
我们首先从最简单的情况开始分析,在此情况中decltype只会鹦鹉学舌般反馈类型:

1
2
3
4
5
const int i =0;//decltype(i) is const int
bool f(const Widget&w);//decltype(w) is const Widget&,decltype(f) is bool(const Widget&)
if(f(w))//decltype(f(w)) is bool
vector<int> v;//decltype(v) is vector<int>
if(v[0]==0)...//decltype(v[0] is int&

看起来一切都很正常。


decltype与返回值

在C++11中,decltype主要用于声明函数返回值依赖于其类型参数的函数模板。举例来说,我们的函数可能会操作一个带有索引的容器,在返回容器内部元素的值之前,我们需要验证一下使用者的身份,此函数的返回值类型显然应当与operator[]的返回值类型一致,此时应当使用decltype。

有人认为在容器类中,operator []总是返回T&。对于deque来说这总是成立的,但对于vector来说,vector<bool>则并非返回一个bool&,而是一个新对象。(代理类,这一问题详见Effective STL Item18及More Effective C++ Item30),这里我们强调的是,一个容器的operator []返回类型不仅仅与其元素类型有关,也与容器自身有关。

下面的实例展示了decltype的用法,它将在后期被进一步改进:

1
2
3
4
5
template<typename Container,typename Index>
auto authAndAccess(Container&c,Index i)->decltype(c[i]){
authenticateUser();
return c[i];
}

函数名前的auto与类型推衍毫无关联,它只是表示此处我们使用了C++11的尾置返回类型(C++ Primer P604)。我们的返回类型取决于c、i,因此我们不可能在没有遇到它们之前声明返回值类型,这即是尾置返回类型的特性与优势所在。

C++11允许使用auto推衍单语句lambda的返回值类型,C++14将其扩展至所有多语句lambda与函数。这意味着在刚才的实例中我们可以不使用尾置返回类型,仅使用auto来完成类型推衍:

1
2
3
4
5
template<typename Container,typename Index>
auto authAndAccess(Container&c,Index i){//not quite correct
authenticateUser();
return c[i];//type deduction take place
}

在Item2中我们曾经说明auto用于函数返回值类型推衍时采用template function推衍法则,这一特点造成了上文实例可能推衍失败的可能,原因在于容器类operator[]的返回类型几乎都是T&,但template function在作类型推衍时将忽略referenceness,如下所示:
1
2
std::deque<int> d;
authAndAccess(d,5) = 10;//can not compile

理论上来说,等式左侧应当是一个int&,但实际上由于template function推衍法则,等式左侧被推衍为int。由于该int是一个函数的返回值,所以它是一个右值,这直接导致了本语句无法编译。


C++14中的decltype(auto)

 
为了保证函数功能正常,我们需要使用decltype来推衍函数返回值类型.C++14引入了decltype(auto)类型说明符,其功能为:auto用以说明当前类型需要被推衍,而decltype则代表了类型推衍所需要采用的法则,修改之后的authAndAccess:

1
2
3
4
5
template<typename Container,typename Index>
decltype(auto) authAndAccess(Container& c,Index i){//still require refinement
authenticateUser();
return c[i];
}

decltype不仅仅可以用来作为返回值类型说明符,也可以用于在初始化语句中定义对象,例如:
1
2
3
4
Widget w;
const Widget& cw = w;
auto myWidget1 = cw;//auto type deduction: myWidget1's type is Widget
decltype(auto) myWidget2 = cw;//decltype deduction:myWidget2's type is const Widget&


authAndAccess的最终修正

 
我们首先回顾一下authAndAccess的声明:

1
2
template<typename Container, typename Index>
decltype(auto) authAndAccess(Container& c, Index i);

容器对象的传递采用reference-non-const形式,如此即可保证用户可通过该函数修改容器内部元素。这种传递方式限制了rvalue的传入,因为rvalue无法被绑定至lvalue引用(除非是lvalue-reference-to-const)。

诚然,传递一个右值容器对象是一种罕见的情况。一个右值容器对象必然是一个临时对象,其在authAndAccess调用完成后必将销毁,那么返回其内部元素的引用也必将造成空悬。但这种情况未必不会发生,例如客户可能希望拷贝一个临时容器中的元素:

1
2
std::deque<std::string> makeStringDeque();//Factory function
auto s = authAndAccess(makeStringDeque(),5);//can't compile

为了解决这个问题,有些开发者也许会想到重载,不过那样我们需要撰写两个函数,这是没必要的,Item24将为我们介绍的universal reference可以同时绑定lvalue与rvalue:
1
2
template<typename Container, typename Index>
decltype(auto) authAndAccess(Container&& c, Index i);

在这个template function中我们对容器类型一无所知,这也直接导致了我们对其索引对象的类型(Index)不甚明确。对未知类型对象采用值传递的方式可能会导致性能下降、slicing等诸多问题,但有时确实存在着pass-by-value的必要,因此我们坚持对Index对象采用pass-by-value。最终,我们采用Item25所建议的std::forward对函数作出修正:
1
2
3
4
5
template<typename Container, typename Index>
decltype(auto)authAndAccess(Container&& c, Index i){
authenticateUser();
return std::forward<Container>(c)[i];
}


decltype的特殊情况

 
将decltype应用于某个名称将导致对此名称的类型推衍,名称是一种左值表达式,不过这不会影响decltype的行为。但是对于比名称更为复杂的左值表达式,decltype将保证类型推衍结果必然是一个左值引用。一般来说这不会造成多大影响,因为左值表达式自带一个引用符,举例来说,一个返回左值的函数即返回左值引用。

请注意下面的例子:

1
int x = 0;

x是变量的名称,因此decltype(x)的结果是int。但如果我们用一个小括号将x包起来形成(x),它就变成了一个左值表达式,那么decltype((x))的结果则会变为int&,但更麻烦的还在后面,有时候这种写法会直接导致崩溃,考虑下面两个函数
1
2
3
4
5
6
7
8
9
decltype(auto) f1(){
int x = 0;
return x;//return int
}

decltype(auto) f1(){
int x = 0;
return (x);//return int& to a local variable!
}

由此可见,表达式类型在一定程度上可能会影响decltype(auto)的推衍结果,Item4将会介绍确保每一次推衍结果都我们所愿的方法。


总结

  1. decltype在作类型推衍时几乎总是不发生任何改变。
  2. 对于并非名称的左值表达式,decltype几乎总是返回T&。
  3. C++14支持decltype(auto),其表示以decltype法则作类型推衍。