这次是对 Effective Modern C++ Item3 的学习笔记。
给定一个名字或者表达式,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 基本的应用就是申明函数模板,它的返回值类型取决于其参数类型。例如,我们有这样一个函数,传入一个容器和下标,期望在函数末尾返回下标指向的值,并且保留返回值的类型。由于容器的 operatpr[] 通常返回一个 T& (但是,std::vector<bool>,opertor[] 返回的不是一个 bool&,这个我们在 Item6 中再讨论),我们期望在函数末尾返回的也是引用类型,以便于我们对其进一步赋值。使用 decltype 达成这样的目标:
template<typename Container, typename Index> // works, but requires refinement
auto authAndAccess(Container& c, Index i)
-> decltype(c[i])
{
authenticateUser();
return c[i];
}
这里使用了拖尾返回值类型的语法,也即在箭头后声明返回值类型,这里可以返回我们期望的引用类型。
C++14可以省略拖尾返回值的声明,这意味着 auto 需要通过编译器根据函数的实现进行类型推导得到:
template<typename Container, typename Index> // C++14; not quite correct
auto authAndAccess(Container& c, Index i)
{
authenticateUser();
return c[i]; // return type deduced from c[i]
}
但是 Item1 和 Item2 的推导规则告诉我们,在模板类型推导中,表达式初始化的引用属性将被忽略。考虑下面的代码片段:
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&,但是根据 auto的类型推导,authAndAccess 函数将会返回一个 int 类型,作为函数返回值,即一个 int 类型的值,这是一个右值,那么对其进行赋值是非法的。
为了得到我们期望的结果,也即返回引用类型,我们需要使用 decltype 的类型推导:decltype(auto),auto 表示类型需要推导,decltype 表示使用 decltype 规则进行推导,我们修改函数实现如下:
template<typename Container, typename Index> // C++14; works, but still requires refinement
decltype(auto) authAndAccess(Container& c, Index i)
{
authenticateUser();
return c[i];
}
这里,authAndAccess 将真正返回 c[i] 的类型,也即 T&。
decltype(auto) 不限于用在函数返回值,也可以用在变量申明,例如:
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&
还有两点需要进一步讨论下,第一点是上面提到的 authAndAccess 还可以进一步优化的事情。让我们再看下 authAndAccess 的声明:
template<typename Container, typename Index>
decltype(auto) authAndAccess(Container& c, Index i);
这里,Container 传参是一个 lvalue-reference-to-non-const,返回值的类型是可以被修改的,这也就意味着我们不能传右值 containers,右值不能绑定到左值引用(除非是 lvalue-references-to-const )。
一个比较好的解决方案是万能引用!如下:
template<typename Container, typename Index>
decltype(auto) authAndAccess(Container&& c, Index i); // c is now a universal reference
在这个模板中,既可以传左值,也可以传右值。但是这里有一个不好地方上是:值传递会造成不必要的拷贝。
为了遵循 Item25 中的警告,我们需要修改下如下:
template<typename Container, typename Index> // final C++14 version
decltype(auto)
authAndAccess(Container&& c, Index i)
{
authenticateUser();
return std::forward<Container>(c)[i];
}
以上需要 C++14 的编译器,C++11 版本的写法:
template<typename Container, typename Index> // final C++11 version
auto authAndAccess(Container&& c, Index i)
-> decltype(std::forward<Container>(c)[i])
{
authenticateUser();
return std::forward<Container>(c)[i];
}
另一问题是开头说的少数场景下,decltype 可能返回不是你所预期的结果。对于一个左值表达式 expr,如果 expr 的类型是 T,decltype(expr) 返回的是 T&。
int x = 0;
decltype(x) 是 int。但是对于 (x) 是一个左值表达式,则decltype((x)) 得到 int&。
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 返回了一个局部变量的引用,这是相当危险的。
因此,在使用 decltype 的时候,建议使用 Item4 中的方法进行检查,确保得到你期望的类型推导。
最后,总结如下: