03 Understand decltype
给一个名字或者表达式,decltype
返回其类型。大部分时候,它按照预期工作,不过极个别场合能让你头疼。
我们先从不会让我们感到惊讶的例子开始。decltype
近乎于鹦鹉学舌。
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&)
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<typename T> // simplified version of std::vector
class vector {
public:
T& operator[](std::size_t index);
};
vector<int> v; // decltype(v) is vector<int>
if (v[0] == 0) // decltype(v[0]) is int&
C++11 中,decltype
的一个常用的地方是写模板函数的返回类型,其依赖于函数参数的类型。比如一个函数接受一个容器对象,其支持索引 operator[]
运算符,返回的就是某个索引对应的对象,那么返回类型应该是该容器索引 operator[]
运算符的返回类型。
通常情况下,类型为 T
的容器的 operator[]
运算符返回类型是 T&
。比如 std::deque
,和大部分的 std::vector
。对于 std::vector<bool>
而言,operator[]
返回的不是 bool&
而是一个新的对象。Item 6 会探究其原因,不过,这里的重点是容器的 operator[]
返回类型依赖于容器。
这种情况下,decltype
能够简化表达。下面是第一个版本,展示了如何使用 decltype
表达返回类型。
template<typename Container, typename Index> // works, but
auto authAndAccess(Container& c, Index i) // requires
-> decltype(c[i]) // refinement
{
authenticateUser();
return c[i];
}
函数开头的 auto
并不表示类型推导。而是 C++11 引入的新语法,返回类型后置到参数列表后的 ->
之后。这样,在写返回类型的时候,可以使用函数参数。authAndAccess
的返回类型就用到了 c
和 i
,如果前置,那么这两个参数不可用。
这样,函数的返回类型和容器 operator[]
运算符返回类型一致了。
C++14 允许对任意函数或 lambda 使用 auto
来推导返回类型,所以上面的例子可以去掉后置返回类型。此时,auto
就是表示类型推导的意思。
template<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]
}
Item 2 解释此时 auto
使用的是模板函数的推导规则。这里就存在这样一个问题。T
类型的容器的 operator[]
往往返回引用类型 T&
,而 Item 1 中说过,模板函数推导往往会忽略引用修饰。这样,如下代码将无法编译。
std::deque<int> d;
authAndAccess(d, 5) = 10; // authenticate user, return d[5],
// then assign 10 to it;
// this won't compile!
d[5]
返回 int&
类型,但是 authAndAccess
的返回类型被推导为 int
,那么函数返回值是一个右值,尝试给右值赋值为 10 是不允许的。
我们使用 decltype
来修复这个问题。C++14 中引入了 decltype(auto)
。auto
是说类型是被推导得到的,decltype
的含义是按照 decltype
的规则推导。代码改写如下。
template<typename Container, typename Index> // C++14; works,
decltype(auto) // but still
authAndAccess(Container& c, Index i) // requires refinement
{
authenticateUser();
return c[i];
}
c[i]
返回类型是 T&
还是新的对象的类型,那么函数 authAndAccess
返回类型就都和 c[i]
一致了。
decltype(auto)
不限于只在函数返回值时使用,想使用 decltype
推导规则来声明变量时也可以用。
Widget w;
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&
现在接着优化之前的函数。其函数签名是
容器通过左值非const
引用传递,返回结果可以被修改,但是无法绑定右值,除非是左值 const
引用方式传递。
当然,传递右值引用是一个不常见的情况。一个右值容器,是一个临时对象,当函数调用结束之后会被销毁,那么拿到一个被销毁的容器的某个对象的引用是很危险的。不过,authAndAccess
可以返回其拷贝。
std::deque<std::string> makeStringDeque(); // factory function
// make copy of 5th element of deque returned from makeStringDeque
auto s = authAndAccess(makeStringDeque(), 5);
一个解决方案是使用函数重载,但是这样需要维护两份代码。另一个解决方案是使用 Item 24 阐述的通用引用,那么函数签名如下
template<typename Container, typename Index> // c is now a
decltype(auto) authAndAccess(Container&& c, Index i); // universal reference
由于不知道容器类型,也就是不知道具体返回对象的类型。如果值传递的话,可能有复制带来的性能问题。不过有的 STL 容器类,比如 vector, string
的 operator[]
也会返回值类型,所以我们也选择这么做。
不过,这里根据 Item 25 的规则对通用引用使用 std::forward
。
template<typename Container, typename Index> // final
decltype(auto) // C++14
authAndAccess(Container&& c, Index i) // version
{
authenticateUser();
return std::forward<Container>(c)[i];
}
回到开头的问题,大部分情况 decltype
会返回我们希望的类型,但是偶尔会给出一个令人吃惊的结果。为了全面理解 decltype
,我们再看几个特殊情况。
decltype
作用于一个变量名,其是左值,没有问题。不过如果作用于一个表达式而不是变量名,总是返回左值引用。这几乎没啥影响。左值表达式自身包含了左值引用的修饰符。
不过有些情况会有些许差异。比如
x
是一个变量名,那么 decltype(x)
返回 int
。如果给 x
加上括号 (x)
这就是个表达式而不是变量名了。x
是左值,(x)
是左值表达式,那么 decltype((x))
返回类型是 int&
。
结合 C++14 的 decltype(auto)
后,情况会更复杂一些。
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
的返回类型不同,后者是 int&
,而且是局部变量的引用,后续使用完全是未定义行为。
这个例子告诉我们要特别注意 decltype(auto)
的使用。看似无关紧要的细节会影响 decltype(auto)
的结果。可以使用 Item 4 中描述的技术确保结果是我们期望的。
同时,不要忽视全局。大部分情况下,不管是否和 auto
配合使用,decltype
只有很多的情况会给出不符合预期的结果。特别是作用于变量名的时候,就是简单返回其类型。
Things to Remember
decltype
almost always yields the type of a variable or expression without any modifications.- For lvalue expressions of type
T
other than names,decltype
always reports a type ofT&
. - C++14 supports
decltype(auto)
, which, likeauto
, deduces a type from its initializer, but it performs the type deduction using thedecltype
rules.