对于给定的名字或表达式,decltype能告诉你名字或表达式型别。一般来说,它告诉你的结果和你预测的是一样的。偶尔它也会给出某个结果,让你抓耳挠腮,不得不去参考手册或在线FAQ页面求得一些启发。
先从一般案例讲起——就是那些不会引发意外的案例。与模板和auto的型别推导过程相反,decltyppe一般只会鹦鹉学舌。返回给定的名字或表达式的确切型别而已:
const int i = 0; //decltype(i)是const intbool f(const Widget& w); //decltype(w)是const Widget&//decltype(f)是bool(const Widget&)struct Point{int x; //decltype(Point::x)是intint y; //decltype(Point::y)是int
};Widget w; //decltype(w)是Widget
if (f(w)) ... //decltype(f(w))是booltemplate<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的主要用途大概就在于声明那些返回值型别依赖于形参型别的函数模板。举个例子,假设我们想要撰写一个函数,其形参中包含一个容器,支持方括号下标语法(即“[]”)和一个下标,并会在返回下标操作结果前进行用户验证,函数的返回值型别须与下标操作结果的返回值型别相同。
一般来说,含有型别T的对象的容器,其operator[]会返回T&,std::deque就属于这种情况,而std::vector也几乎总是属于这种情况。只有std::vector<bool>对应的operator[]并不返回bool&,而返回一个全新对象。至于这样处理的原因和具体处理结果,会在后面讨论。而decltype使得这样的意思表达简单易行。下面我们撰写该模板的首次尝试,其中演示了使用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。如果我们还是使用传统的返回值型别先序语法,那c和i会由于还未声明,从而无法使用。
采用了这么一个声明形式以后,operator[]返回值是什么型别,authAndAccess的返回值就是什么类型,和我们期望的结果一致。
C++11允许对单表达式的lambda式的返回值型别实施推导,而C++14则将这个允许范围扩张到了一切lambda和一切函数,包括那些多表达式的。对于antuAndAccess这种情况来说,这:就意味着在C++14中可以去掉返回值型别尾序语法,而只保留前导auto.在那样的声明形式中,auto确实说明会发生型别推导。具体地说,它说明编译器会根据函数实现来实施函数返回值的型别推导 :
template<typename Container, typename Index> //C++14
auto authAndAccess(Container& c, Index i) //不甚正确
{authenticateUser();return 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++的监护人们,由于预见到在进行某些型别推倒时需要采用decltype型别推导规则,在C++14中通过decltype(auto)饰词解决了这个问题。乍看上去自相矛盾(又是auto又是decltype),其实完全合情合理:auto指定了欲实施推导的型别,而推导过程中采用的是decltype的规则。总而言之,我们可以这样撰写authAndAccess:
template<typename Container, typename Index>
decltype(auto)
authAndAccess(Container& c, Index i)
{authenticateUser();return c[i];
}
现在,authAndAccess的返回值型别真的和c[i]返回的型别一致了。具体地说,一般情况下c[i]返回T&,authAndAccess也会返回T&,而对于少见情况,c[i]返回一个对象型别,anthAndAccess也会亦步亦趋地返回对象型别。
decltype(auto)并不限于在函数返回值型别处使用。在变量声明的场合上,若你也想在初始化表达式处应用decltype型别推导规则,也可以照样便宜行事:
Widget w;
const Widget& cw = w;
auto myWidget1 = cw; //auto型别推导:myWidget1的型别是Widget
decltype(auto) myWidget2 = cw; //decltype型别推导:myWidget2的型别是const Widget&
不过,我知道现在还有两个烦恼萦绕在你的脑际,一个是前面说的对authAndAccess的改进,这个还一直憋着没说,现在就说下这个问题。
再看一遍C++14版本的authAndAccess:
template<typename Container, typename Index>
decltype(auto) authAndAccess(Container& c, Index i)
容器的传递方式是对非常量的左值引用,因为返回该容器的某个元素的引用,就意味着允许客户对容器进行修改。不过这也意味着无法向该函数传递右值容器,右值是不能绑定到左值引用的(除非是对常量的左值引用,与本例情况不符) 。
必须承认,向authAndAccess传递右值容器属于罕见情况。一个右值容器,作为一个临时对象,一般而言会在包含了调用authAndAccess的语句结束处被析构,而这就是说,该容器中某个元素的引用(这是authAndAccess一般情况下会返回的)会在创建它的那个语句结束时被置于空悬状态。但即使如此,向authAndAccess传递一个临时对象仍然可能是合理行为。客户可能就是想要制作该临时容量的某元素的一个副本,请看下例:
std::deque<std::string> makeStringDeque(); //工厂函数//制作makeStringDeque返回的deque的第5个元素的副本
auto s = authAndAccess(makeStringDeque(), 5);
容器的传递方式是对非常量的左值引用,因为返回该容器的某个元素的引用,就意味着允许客户对容器进行修改。不过这也意味着无法向该函数传递右值容器。右值是不能绑定到左值的引用的(除非是对常量的左值引用,用本例情况不符)。
如果支持这种用法,就得修订authAndAccess的声明,以同时接受左值和右值。重载是个办法(一个重载版本声明一个左值引用形参,另一个重载版本声明一个右值引用形参),但这么一来就需要维护两个函数。避免这个后果的一个方法就是让authAndAccess采用一种既能够绑定到左值也能够绑定到右值的引用形参,这正是万能引用大显身手之处。
template<typename Container, typename Index>
decltype(auto) authAndAccess(Container&& c, Index i) //c现在是个万能引用了
在本模板中,我们对于操作的容器型别并不知情,同时对下标对象型别也一样不知情。对未知型别的对象采用按值传递有着诸多风险:非必要的复制操作带来的性能隐患、对象截切问题带来的行为异常,还有同行的嘲讽等,但在特定容器下标的这个特定问题上,遵循标准库中给出的下标示例(例如std::string std::vector和std::deque的operator[])应该是合理的,所以这里坚持使用了按值传递。
对万能引用要应用std::forward:
//c++14最终版
template<typename Container, typename Index>
decltype(auto) authAndAccess(Container&& c, Index i)
{authenticateUser();return std::forward<Container>(c)[i];
}//c++11版本
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&,仅仅把一个名字放入一对小括号中,就改变了decltype的推导结果!请看以下示例(C++14)
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(auto)时需要及其小心翼翼,看似是以推导型别表达式的写法这样无关紧要的细节,却影响了decltype(auto)得出的结果。
总结:
- 绝大多数情况下,decltype会得出变量或表达式的型别而不作任何修改
- 对于型别为T的左值表达式,除非该表达式仅有一个名字,decltype总是得出型别T&
- C++14支持decltype(auto),和auto一样,它会从其初始化表达式出发来推导型别,但是它的型别推导使用的是decltype的规则。