Item 3: Understand decltype
Tuesday, 22. August 2017 11:02AM
#Item 3: Understand decltype
#条目3: 理解decltype
##术语&概念
- ###decltype
对于一个给定的名称或表达式,decltype会告诉你这个名称或表达式的类型。
##快速笔记
1. 大多数情况下decltype会准确的给出你所预期的类型
2. 在C++11中,decltype可能的主要用途就是在某种情况下用来声明函数模板的返回值,这里特指在这个函数的返回值类型取决于它的形参类型的情况下。
- trailing return type 语法
- C++14 的前置decltype语法
- 修订一:universal reference
- 修订二:perfect forward
3. decltype制造“惊喜”的情况:当对名称以外的更复杂的东西使用decltype时,结果可能有点意外。比如decltype(x)和decltype((x))。
##例子
==Example 1==
这里的例子中decltype不出所料地,鹦鹉学舌般准确地回答了所给出的名字或是表达式的类型,没有制造意外。
~~~
count int i = 0; // decltype(i) is const int
bool f(const Widget& w); // decltype(w) is const Widget&
// decltype(f) is bool(const Widget&)
struct Point {
int x, y; //decltype(Point::x) is int
}; //decltype(Point::y) is int
Widget w; //decltype(w) is Widget
if (f(w)) … //decltype(f(w)) is bool
template
vector
==Example 2==
下面来看decltype如何运用于声明函数模板。需要注意的是这种实现可以工作但是还能做一些改进,此处先按下不表。
~~~
template<typename Container, typename Index> // works, but
auto authAndAccess(Container& c, Index i) // requires
-> decltype(c[i]) // refinement
{
authenticateUser();
return c[i];
}
~~~
这里auto类型指示符(type-specifier)并没有做什么与类型推断相关的事情。它其实指明了C++11的后置返回类型(trailing return type)语法在此处被使用。
这种语法的好处是你可以使用形参列表里的参数来表达自己让编译器推断的类型,以往的前置返回值无法获知形参,因为那个时候形参们尚未被声明。
此种情况中在C++11 中必须要写上这种特殊语法,但是在C++14中,这样的后置语法可以忽略,编译器会自动去推断return 之后的表达式的类型作为返回类型。例子如下,同时要注意如下的例子其实并不正确。
~~~
templates <typename Container, typename Index> //C++14
auto authAndAccess(Container& c, Index i) //not quite
{ //correct
authenticateUser();
return c[i]; //return type deduced from c[i]
}
~~~
条目2向我们解释了对于函数的auto返回类型推断实际上使用了模板类型推断。在这个例子里等价的模板推断如下(仅笔者理解,待考),下述例子中T的推断结果便是上面这个片段里的返回类型推断结果。
~~~
template
std::deque
Case 3: 当paramType既不是指针也不是引用时,
- 如果实参expr的类型是reference,忽略reference的部分。
- 如果忽略reference之后还有const,一并忽略。volatile也忽略。最后将expr与paramType匹配。
根据上述诀窍我们来看看这个例子中T的推断结果: d[5]的类型是int&, 忽略掉该忽略的部分以后还剩下int,paramType的类型是T,因此T的类型被推断为int。这种结果在我们这里例子中是会带来问题的。int作为一个函数的返回类型是一个右值(rvalue)。也意味着以下代码编译无法通过,
~~~
std::deque
const Widget& cw = w;
auto myWidget1 = cw; // auto type deduction
// myWidget1’s type is Widget
decltype(auto) myWidget2 = cw; // decltype type deduction;
// myWidget2’s type is
// const Widget&
~~~
接下来我们讲讲之前提到的这个函数模板的改进问题。
####修订一:universal reference
来观察C++14版的authAndAccess声明
~~~
template<typename Container, typename Index>
decltype(auto) authAndAccess(Container& c, Index i);
~~~
我们可以发现只有容器的非const左值引用(lvalue-reference-to-non-const)可以被传为第一参数。
右值(rvalue)并不能作为这里的参数。
不可否认的是,给authAndAccess传递右值容器是一种极端情况。因为右值容器是一个临时对象,通常会在包含authAndAccess调用的语句结束时就被销毁,同时意味着对这个容器内某个元素的引用也会被悬空。
但是如果说用户仅仅是想利用到这个临时容器的某个元素的拷贝也是说得通的。举个例子:
~~~
std::deque
// make copy of 5th element of deque returned
// from makeStringDeque
auto s = authAndAccess(makeStringDeque(),5);
~~~
为了支持这样的使用方式意味要改进authAndAccess的声明使得可以同时处理lvalue和rvalue。Overloading是一个选择,但是我们需要维护两个函数,一个以lvalue reference为形参,另一个以rvalue reference为形参。
避免这个问题的方法是:使用universal reference。我们可以作如下的函数模板声明
~~~
template<typename Container, typename Index> //c is now
decltype(auto) authAndAccess(Container&& c, Index i); // universal reference
~~~
注意到这里对Index这个类型未知的对象使用了按值传参(pass-by-value). 通常按值传参存在三个风险:
- 不必要的对象拷贝导致性能(performance)上的忧虑
- 存在发生对象剪切(object slicing)(见Item 41)的风险。
- 来自同侪的嘲讽(derision)
但是就这个具体问题(一个容器的索引)而言,沿用标准库的做法(eg. operator[] for std::string, std::vector, std::deque)看起来是合理的。所以我们这里对索引坚持使用了pass-by-value.
####修订二: perfect forward
然而,根据Item 25给我们的告诫:为universal reference使用std::forward
我们仍需做出以下改进
~~~
template<typename Container, typename Index> // final C++14 version
decltype(auto)
authAndAccess(Container&& c, Index i)
{
authenticateUser();
return std::forward
==Example 3==
decltype几乎总是能产生你所预期的类型,少数情况下会制造“惊喜”。说实话,你不太可能碰到这些规律的例外情况,除非你是个负有重大责任(heavy-duty)的库实现者。
要完全理解掌握decltype的行为,你不得不使自己熟悉极少的特例。大多数的特例过于晦涩,我们按下不表。但是我们可以来看一个例子来更深入了解一下decltype和它的用法。
~~~
int x = 0;
~~~
x
是一个变量的名称,所以decltype(x)
是int, 但是如果将x
用括号包裹起来变成(x)
,产生了一个比单纯名称要复杂一点表达式时,情况有所不同。作为一个名称,x
是一个左值(lvalue),而且C++也将表达式(x)
定义为左值。因此decltype((x))
的结果是int&
。
在C++11中, 这个现象只是一个令人好奇的小玩意。但是在结合和C++14的decltype(auto)
的语法之后,这意味着你对return语句进行一个看似无关紧要的改动也能影响到函数返回值的类型推断结果。见下例
~~~
decltype(auto) f1()
{
int x = 0;
…
return x; //decltype(x) is int, so f1 returns int
}
decltype(auto) f2()
{
int x = 0;
…
return (x); //decltype((x)) is int&, so f2 returns int&
}
~~~
要注意到这不仅是f2的返回类型和f1不同的问题。f2返回了一个局部变量的引用!像这样的代码将你丢到了名为未定义行为(undefined behavior)的特快列车上, 一辆你肯定不想上的列车。
重要的告诫就是,当你使用decltype(auto)的时候要非常当心。
##谨记要点
- decltype几乎总是不加改动地产生一个变量或是表达式的类型。
- 对类型为T的,除了名称(name)以外的其他左值表达式(lvalue expression),decltype总是报告一个T&
类型。
- C++14支持decltype(auto)
语法。这就像auto一样,从初始化器推断一个类型,只不过它执行的是decltype的推断规则。(jtpan:去推断 declared type,而不是auto的规则:函数模板返回类型推断的规则)