前置:decltype
decltype是C++11新增关键字,和auto的功能一样,用来在编译时期进行自动类型推导。引入decltype是因为auto并不适用于所有的自动类型推导场景,在某些特殊情况下auto用起来很不方便,甚至压根无法使用。
auto varName=value;
decltype(exp) varName=value;
auto根据=右边的初始值推导出变量的类型,decltype根据exp表达式推导出变量的类型,跟=右边的value没有关系。auto要求变量必须初始化,这是因为auto根据变量的初始值来推导变量类型的,如果不初始化,变量的类型也就无法推导。decltype不要求,因此可以写成如下形式
decltype(exp) varName;
exp只是一个普通的表达式,它可以是任意复杂的形式,但必须保证exp的结果是有类型的,不能是void;如exp为一个返回值为void的函数时,exp的结果也是void类型,此时会导致编译错误。
int x = 0;
decltype(x) y = 1; // y->int
decltype(x + y) z = 0;// z->int
const int& i = x;
decltype(i) j = y; // j->const int &
const decltype(z) * p = &z; //*p->const int,p->const int *
decltype(z) * pi = &z; // *pi->int, pi->int*
decltype(pi)* pp = π // *pp->int*,pp->int**
(1)如果exp是一个不被括号()包围的表达式,或者是一个类成员访问表达式,或者是一个单独的变量,decltype(exp)的类型和exp一致
(2)如果exp是函数调用,则decltype(exp)的类型就和函数返回值的类型一致
(3)如果exp是一个左值,或被括号()包围,decltype(exp)的类型就是exp的引用,假设exp的类型为T,则decltype(exp)的类型为T&。
对于规则三的说明:
class A{
public:
int x;
}
int main(){
const A obj;
decltype(obj.x) a=0;//a的类型为int
decltype((obj.x)) b=a;//b的类型为int&
int n=0,m=0;
decltype(m+n) c=0;//n+m得到一个右值,c的类型为int
decltype(n=n+m) d=c;//n=n+m得到一个左值,d的类型为int &
return 0;
}
前置:尾置返回类型
函数的返回类型通常需要在函数声明时明确指定。当涉及到模板函数时,有时返回类型可能依赖于模板参数的类型,这很难直接指定返回类型。C++11引入了尾置类型推导,允许在函数参数列表之后使用auto关键字和decltype来指定返回类型。
template<typename T, typename U>
auto add(T t, U u) -> decltype(t + u) {
return t + u;
}
item3结论:
decltype
总是不加修改的产生变量或者表达式的类型。- 对于
T
类型的不是单纯的变量名的左值表达式,decltype
总是产出T
的引用即T&
。 - C++14支持
decltype(auto)
,就像auto
一样,推导出类型,但是它使用decltype
的规则进行推导。
相比模板类型推导和auto
类型推导(参见item1和item2),decltype
只是简单的返回名字或者表达式的类型:
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> //std::vector的简化版本
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&
,它会返回一个全新的对象(译注:MSVC的STL实现中返回的是std::_Vb_reference<std::_Wrap_alloc<std::allocator<unsigned int>>>
对象)(item6)。对一个容器进行operator[]
操作返回的类型取决于容器本身。
使用decltype
使得我们很容易去实现它,使用decltype
计算返回类型,这个模板需要改良.
template<typename Container, typename Index>
auto authAndAccess(Container& c, Index i)->decltype(c[i]){
authenticateUser();
return c[i];
}
函数前面auto
不做任何的类型推导工作。相反的,auto只暗示使用C++11的尾置返回类型语法,即在函数形参列表后面使用一个->
符号指出函数的返回类型,尾置返回类型的好处是可以在函数返回类型中使用函数形参相关的信息。
在authAndAccess
函数中,使用c
和i
指定返回类型。如果按照传统语法把函数返回类型放在函数名称之前,c
和i
就未被声明所以不能使用。
C++11允许自动推导单一语句的lambda表达式的返回类型, C++14扩展到允许自动推导所有的lambda表达式和函数,甚至它们内含多条语句。
对于authAndAccess
来说在C++14标准下我们可以忽略尾置返回类型,只留下一个auto
。使用这种声明形式,auto标示这里会发生类型推导。更准确的说,编译器将会从函数实现中推导出函数的返回类型。
template<typename Container, typename Index> //C++14版本,
auto authAndAccess(Container& c, Index i) //不那么正确
{
authenticateUser();
return c[i]; //从c[i]中推导返回类型
}
Item2解释了函数返回类型中使用auto
,编译器实际上是使用的模板类型推导的那套规则。如果那样的话这里就会有一些问题。正如我们之前讨论的,operator[]
对于大多数T
类型的容器会返回一个T&
,但是Item1解释了在模板类型推导期间,表达式的引用性(reference-ness)会被忽略。
std::deque<int> d;
…
authAndAccess(d, 5) = 10; //然后把10赋值
d[5]
本该返回一个int&
,但是模板类型推导会剥去引用的部分,因此产生了int
返回类型。函数返回的那个int
是一个右值,上面的代码尝试把10赋值给右值int
,C++11禁止这样做,所以代码无法编译。
要想让authAndAccess
像我们期待的那样工作,需要使用decltype
类型推导来推导它的返回值。
C++期望在某些情况下当类型被暗示时需要使用decltype
类型推导的规则,C++14通过使用decltype(auto)
说明符使得这成为可能。decltype(auto)
可能觉得非常的矛盾(到底是decltype
还是auto
?),实际上我们可以这样解释它的意义:auto
说明符表示这个类型将会被推导,decltype
说明decltype
的规则将会被用到这个推导过程中。
template<typename Container, typename Index> //C++14版本,
decltype(auto) //可以工作,
authAndAccess(Container& c, Index i) //但是还需要
{ //改良
authenticateUser();
return c[i];
}
authAndAccess
将会真正的返回c[i]
的类型。一般情况下c[i]
返回T&
,authAndAccess
也会返回T&
,特殊情况下c[i]
返回一个对象,authAndAccess
也会返回一个对象。decltype(auto)
的使用不仅仅局限于函数返回类型,当想对初始化表达式使用decltype
推导的规则。
Widget w;
const Widget& cw = w;
auto myWidget1 = 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);
容器通过传引用的方式传递非常量左值引用,因为返回一个引用允许用户可以修改容器。但是这意味着不能给这个函数传递右值容器,右值不能被绑定到左值引用上(除非这个左值引用是一个const(,但是这里明显不是)。
公认的向authAndAccess
传递一个右值是一个小概率事件。一个右值容器,是一个临时对象,通常会在authAndAccess
调用结束被销毁,这意味着authAndAccess
返回的引用将会成为一个悬置的引用。但是使用向authAndAccess
传递一个临时变量也并不是没有意义,有时候用户可能只是想简单的获得临时容器中的一个元素的拷贝,比如这样:
std::deque<std::string> makeStringDeque(); //工厂函数
//从makeStringDeque中获得第五个元素的拷贝并返回
auto s = authAndAccess(makeStringDeque(), 5);
要想支持这样使用authAndAccess
就得修改一下当前的声明使得它支持左值和右值。重载是一个不错的选择(一个函数重载声明为左值引用,另一个声明为右值引用),但是我们就不得不维护两个重载函数。另一个方法是使authAndAccess
的引用可以绑定左值和右值,Item24解释了那正是通用引用能做的,所以我们这里可以使用通用引用进行声明:
template<typename Containter, typename Index> //现在c是通用引用
decltype(auto) authAndAccess(Container&& c, Index i);
在这个模板中,我们不知道我们操纵的容器的类型是什么,那意味着我们同样不知道它使用的索引对象(index objects)的类型,对一个未知类型的对象使用传值通常会造成不必要的拷贝,对程序的性能有极大的影响,还会造成对象切片行为(参见item41)。但是容器索引来说,我们遵照标准模板库对于索引的处理是有理由的(比如std::string
,std::vector
和std::deque
的operator[]
),所以我们坚持传值调用。需要更新一下模板的实现,让它能听从Item25的告诫应用std::forward
实现通用引用:
template<typename Container, typename Index> //最终的C++14版本
decltype(auto) authAndAccess(Container&& c, Index i){
authenticateUser();
returnstd::forward<Container>(c)[i];
}
template<typename Container, typename Index> //最终的C++11版本
auto authAndAccess(Container&& c, Index i)
->decltype(std::forward<Container>(c)[i]){
authenticateUser();
returnstd::forward<Container>(c)[i];
}
另一个问题是就像我在条款的开始唠叨的那样,decltype
通常会产生你期望的结果,但并不总是这样。在极少数情况下会出现歧义。