前言
decltype是一项奇特的技术,它能够告诉你某个名称或表达式的类型,尽管有时候返回的结果令你抓耳挠腮。
decltype的使用
我们首先从最简单的情况开始分析,在此情况中decltype只会鹦鹉学舌般反馈类型:
1 | const int i =0;//decltype(i) is const int |
看起来一切都很正常。
decltype与返回值
在C++11中,decltype主要用于声明函数返回值依赖于其类型参数的函数模板。举例来说,我们的函数可能会操作一个带有索引的容器,在返回容器内部元素的值之前,我们需要验证一下使用者的身份,此函数的返回值类型显然应当与operator[]的返回值类型一致,此时应当使用decltype。
有人认为在容器类中,operator []总是返回T&。对于deque来说这总是成立的,但对于vector来说,vector<bool>则并非返回一个bool&,而是一个新对象。(代理类,这一问题详见Effective STL Item18及More Effective C++ Item30),这里我们强调的是,一个容器的operator []返回类型不仅仅与其元素类型有关,也与容器自身有关。
下面的实例展示了decltype的用法,它将在后期被进一步改进:
1 | template<typename Container,typename Index> |
函数名前的auto与类型推衍毫无关联,它只是表示此处我们使用了C++11的尾置返回类型(C++ Primer P604)。我们的返回类型取决于c、i,因此我们不可能在没有遇到它们之前声明返回值类型,这即是尾置返回类型的特性与优势所在。
C++11允许使用auto推衍单语句lambda的返回值类型,C++14将其扩展至所有多语句lambda与函数。这意味着在刚才的实例中我们可以不使用尾置返回类型,仅使用auto来完成类型推衍:
1 | template<typename Container,typename Index> |
在Item2中我们曾经说明auto用于函数返回值类型推衍时采用template function推衍法则,这一特点造成了上文实例可能推衍失败的可能,原因在于容器类operator[]的返回类型几乎都是T&,但template function在作类型推衍时将忽略referenceness,如下所示:
1 | std::deque<int> d; |
理论上来说,等式左侧应当是一个int&,但实际上由于template function推衍法则,等式左侧被推衍为int。由于该int是一个函数的返回值,所以它是一个右值,这直接导致了本语句无法编译。
C++14中的decltype(auto)
为了保证函数功能正常,我们需要使用decltype来推衍函数返回值类型.C++14引入了decltype(auto)类型说明符,其功能为:auto用以说明当前类型需要被推衍,而decltype则代表了类型推衍所需要采用的法则,修改之后的authAndAccess:
1 | template<typename Container,typename Index> |
decltype不仅仅可以用来作为返回值类型说明符,也可以用于在初始化语句中定义对象,例如:
1 | Widget w; |
authAndAccess的最终修正
我们首先回顾一下authAndAccess的声明:
1 | template<typename Container, typename Index> |
容器对象的传递采用reference-non-const形式,如此即可保证用户可通过该函数修改容器内部元素。这种传递方式限制了rvalue的传入,因为rvalue无法被绑定至lvalue引用(除非是lvalue-reference-to-const)。
诚然,传递一个右值容器对象是一种罕见的情况。一个右值容器对象必然是一个临时对象,其在authAndAccess调用完成后必将销毁,那么返回其内部元素的引用也必将造成空悬。但这种情况未必不会发生,例如客户可能希望拷贝一个临时容器中的元素:
1 | std::deque<std::string> makeStringDeque();//Factory function |
为了解决这个问题,有些开发者也许会想到重载,不过那样我们需要撰写两个函数,这是没必要的,Item24将为我们介绍的universal reference可以同时绑定lvalue与rvalue:
1 | template<typename Container, typename Index> |
在这个template function中我们对容器类型一无所知,这也直接导致了我们对其索引对象的类型(Index)不甚明确。对未知类型对象采用值传递的方式可能会导致性能下降、slicing等诸多问题,但有时确实存在着pass-by-value的必要,因此我们坚持对Index对象采用pass-by-value。最终,我们采用Item25所建议的std::forward对函数作出修正:
1 | template<typename Container, typename Index> |
decltype的特殊情况
将decltype应用于某个名称将导致对此名称的类型推衍,名称是一种左值表达式,不过这不会影响decltype的行为。但是对于比名称更为复杂的左值表达式,decltype将保证类型推衍结果必然是一个左值引用。一般来说这不会造成多大影响,因为左值表达式自带一个引用符,举例来说,一个返回左值的函数即返回左值引用。
请注意下面的例子:
1 | int x = 0; |
x是变量的名称,因此decltype(x)的结果是int。但如果我们用一个小括号将x包起来形成
(x)
,它就变成了一个左值表达式,那么decltype((x))的结果则会变为int&,但更麻烦的还在后面,有时候这种写法会直接导致崩溃,考虑下面两个函数1 | decltype(auto) f1(){ |
由此可见,表达式类型在一定程度上可能会影响decltype(auto)的推衍结果,Item4将会介绍确保每一次推衍结果都我们所愿的方法。
总结
- decltype在作类型推衍时几乎总是不发生任何改变。
- 对于并非名称的左值表达式,decltype几乎总是返回T&。
- C++14支持decltype(auto),其表示以decltype法则作类型推衍。