const int i = 0; //decltype(i)是const int
bool f(const Widget& w); //decltype(w)是const Widget&
//decltype(f)是bool(const Widget&)
struct Point{
int x,y; //decltype(Point::x)是int
}; //decltype(Point::y)是int
Widget w; //decltype(w)是Widget
if(f(w)); //decltype(f(w))是bool
template<typename T>
class vector{
public:
T& operator[](std::size_t index);
};
vector<int> v; //decltype(v)是vector<int>
if(v[0] == 0) //decltype(v[0])是int&
c++11中,decltype
的主要用途大概就在于声明那些返回值类型依赖于形参类型的函数模板。
template<typename Container, typename Index>
auto authAndAccess(Container& c, Index i) -> decltype(c[i])
{
authenticateUser();
return c[i];
}
在函数名字前面使用的那个auto
和类型推导没有任何关系。它只为说明这里使用了C++11中的尾置返回值类型语法。
尾置返回类型的好处在于,在指定返回值类型时可以使用函数形参。比如,在authAndAccess
中,在指定返回值类型时就可以使用c
和i
。
采用了这样一个形式后,operator[]
返回值是什么类型,authAndAccess
的返回值就是什么类型。
c++11允许对单表达式的lambda
式的返回值实施推导,c++14则将这个允许范围扩张到了一切lambda
式和一切函数。对于authAndAccess
这种情况来说,这就意味着在c++14中可以去掉尾置返回类型语法,只保留auto
。
template<typename Container, typename Index>
auto authAndAccess(Container& c, Index i)
{
authenticateUser();
return c[i]; //返回值类型是根据c[i]推导出来的
}
条款2解释说,编译器会对auto
指定为返回类型的函数实现模板类型推导。这样会留下隐患,大多是含有类型T
的对象的容器的operator[]
会返回T&
。条款1解释说,模板类型推导过程中,初始化表达的引用性会被忽略。
std::deque<int> d;
authAndAccess(d, 5) = 10; //验证用户,并返回d[5]
//然后将其赋值为10
//这段代码无法通过编译
d[5]
返回的是int&
,但是对authAndAccess
的返回值实施auto
类型推导将剥去引用饰词,这样一来返回值就变成了int
。作为函数的返回值,该int
是个右值,所以上述代码其实尝试将10赋给一个右值int
。这在c++中属于被禁止的行为,所以代码无法通过编译。
想要让authAndAccess
如我们期望般运行,就要对其返回值实施decltype
类型推导,即指定authAndAccess
的返回值类型与表达式c[i]
返回的类型完全一致。
在c++14中通过decltype(auto)
饰词解决了这个问题。
template<typename Container, typename Index>
decltype(auto) authAndIndex(Container& c, Index i)
{
authenticateUser();
return c[i];
}
现在,authAndAccess
的返回值类型真的与c[i]
返回的类型一致了。
decltype(auto)
并不限于在函数返回值类型处使用。在变量声明的场合上,也可以使用。
Widget w;
const Widget& cw = w;
auto myWidget = cw; //auto类型推导
//myWidget1的类型是Widget
decltype(auto) myWidget2 = cw; //decltype类型推导
//myWidget2的类型是const Widget&
再看一遍C++14版本的authAndAccess
:
template<typename Container, typename Index>
decltype(auto) authAndAccess(Container& c, Index i);
容器的传递方式是对非常量的左值引用,因为返回该容器的某个元素的引用,就意味着允许客户对容器进行修改。不过这也意味着无法向该函数传递右值容器。右值是不能绑定到左值引用的(除非是对常量的左值引用)。
必须承认,向authAndAccess
传递右值容器属于罕见情况。一个右值容器,作为一个临时对象,一般而言会在包含了调用authAndAccess
的语句结束处被析构。而这就是说,该容器中某个元素的引用会在创建它的那个语句结束时被置于空悬状态。但即使如此,向authAndAccess
传递一个临时对象仍然可能是合理行为。客户可能就是想要制作该临时容器的某元素的一个副本。
std::deque<std::string> makeStringDeque(); //工厂函数
auto s = authAndAccess(makeStringDeque(), 5);
容器的传递方式是对非常量的左值引用,因为返回该容器的某个元素的引用,因为返回该容器的某个元素的引用,就意味着允许客户对容器进行修改。不过这也意味着无法向该函数传递右值容器。右值不能绑定到左值引用。
如果要支持这种用法,就得修改authAndAccess
的声明,同时接受左值和右值。可以声明成万能引用。对万能引用要应用std::forward
。
template<typename Container, typename Index>
decltype(auto) authAndAccess(Container&& c,Index i)
{
authenticateUser();
return std::forward<Container>(c)[i];
}
以下是C++11的版本。它和C++14版本几乎一样,只是你需要自行指定返回值类型。
template<typename Container, typename Index>
auto authAndAccess(Container&& c, Index i)->decltype(std::forward<Container>(c)[i])
{
authenticateUser();
return std::forward<Container>(c)[i];
}
将decltype
应用于一个名字之上,就会得出该名字的声明类型。名字其实是左值表达式,但如果仅有一个名字,decltype
的行为保持不变。
如果是更复杂的左值表达式,decltype
就保证得出的类型总是左值引用。换言之,只要一个左值表达式不仅是一个类型为T
的名字,它就得出一个T&
类型。
int x = 0;
其中x是一个变量名字,所以decltype(x)
的结果是int
。但是如果把x放入一对小括号中,就得到了一个比仅有名字更复杂的表达式(x)
。作为一个名字,x是一个左值,在c++定义中,表达式(x)
也是一个左值,所以decltype((x))
的结果就是int&
。
在c++14中,上述的性质如果和decltype(auto)
联合一下,就会影响到函数的类型推导结果。
decltype(auto) f1()
{
int x = 0;
return x; //decltype(x)是int,所以f1返回的是int
}
decltype(auto) f2()
{
int x = 0;
return (x); //decltype((x))是int&&,所以f2返回的是int&
}
f2
返回了一个局部变量的引用。这是错误的。
要点速记
- 绝大多数情况下,
decltype
会得出变量或表达式的类型而不做任何修改 - 对于类型为
T
的左值表达式,除非该表达式仅有一个名字,decltype
总是得出类型T&
- c++14支持
decltype(auto)
,和auto
一样,它会从初始化表达式出发来推导类型,但是它的类型推导使用的是decltype
的规则