Item3:理解decltype
decltype
是一个古怪的创造。给一个名字或表达式,decltype
告诉你这个名字或表达式的类型。通常,它告诉你的就是你所预测的。但是偶尔,它提供的结果会让你抓破头让你求助于参考书或在线Q&A网站来获得启示。
我们以典型的案列作为开始—它并不让人惊喜。与在对模板和auto
类型推导期间发生了什么的情况想比较(见条款1和条款2),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
最基本的用法就是用于声明那些返回类型依赖参数类型的函数模板。例如,假设我们想要写一个有支持通过方括号索引的容器的函数(换句话说,使用“[]”),然后在返回索引操作结果之前验证用户。函数的返回类型与索引操作返回类型相同。
operator[]
作用在类型为T
的容器上通常返回T&
。例如std::deque
就是这样,而std::vector
几乎总是这样。但是,对于std::vector<bool>
,operator[]
并不是返回bool&
。而是返回一个全新的对象。我们将在条款6中探究为什么会有这种情况,在这里更重要的是容器的operator[]
的返回类型依赖于容器。
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
是不可用的,因为它们还没有被声明。
使用这样的声明,正如我们期望的,当申请传入容器时,authAndAccess
返回任何operator[]
返回的类型。
C++11允许对单句匿名表达式返回类型进行推导,C++14将这个特性进一步扩展到所有的匿名表达式和函数之中。在authAndAccess
的例子中,这意味C++14中我们可以省略后置的返回类型,只剩下头部的auto
。使用这种形式的声明,意味着会发生类型推导。特别的,这意味着编译器将从函数的实现推导函数的返回类型:
template <typename Container, typename Index> //C++14;
auto authAndAccess(Container& c, Index i) //不是很
{ //正确
authenticateUser();
return c[i];
}
条款2解释了对于用auto
返回类型说明符的函数,编译器会使用模板函数推导。但是在这个例子中,存在一个问题。正如我们已经讨论的,对于大多数类型为T
的容器,operator[]
返回T&
,但条款1解释了在类型推导期间,初始化的表达式的reference-ness
被忽略。思考对于这样的客户代码那会发生什么:
std::deque<int> d;
...
authAndAccess(d, 5) = 10; //认证用户,返回d[5],
//然后将10赋给返回值,
//这将不会编译!
这里,d[5]
返回的为int&
类型,但auto
对authAndAccess
的返回类型推导将去除引用,因此产生的返回类型为int
。那个int
,将作为函数的返回值,是个右值,上述的代码尝试将10赋值给一个int
类型的右值。这在C++中是禁止的,因此代码将不会编译。
为了让authAndAccess
像我们期望的一样,我们需要对返回值使用decltype
类型推导,换句话说,去指定authAndAccess
返回的类型与表达式c[i]
的一样。C++的拥护者期望在类型推导的情况下使用decltype
类型推导规则,在C++14中通过decltype(auto)
说明符让这个成为了可能。最初这看起来是矛盾的(decltype
和auto
?),实际上可以合得来:auto
指定需要推导的类型,decltype
说明应在推导期间使用decltype
规则。因此我们可以想这样的改写authAndAccess
:
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类型推导:
//myWidget的类型为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);
传入容器的是非const
左值引用,因为返回一个容器中的元素的引用允许修改这个容器。但这意味着不可传右值容器给这个函数。右值不能绑定到左值引用(除非是常量左值引用,但这里不是这种情况)。
无可否认地,传递右值容器给authAndAccess
是一个极端的例子。右值容器,是一个临时的对象,通常将在包含authAndAccess
调用的语句的末尾处被销毁,这意味在在那个容器容器里的元素的引用(通常由authAndAccess
返回)将在创造它的语句的末尾被空悬。然而,传一个临时对象给authAndAccess
还是有意义的。例如:一个客户仅仅想要临时容器元素的拷贝:
std::deque<std::string> makeStringDeque(); //工厂函数
//从makeStringDeque返回的
//双端队列的第5个元素的拷贝
auto s = authAndAccess(makeStringDeque(), 5);
支持这样的使用意味着我们要修改authAndAccess
的声明同时接收左值和右值。重载能够达到目的(一个重载声明为左值引用参数,另一个声明为右值引用参数),但我们要维护两个函数。避免这样的方法是authAndAccess
使用一个引用参数,它能够绑定到左值也能绑定到右值,在条款4中解释了通用引用是如何做到的。因此,authAndAccess
能想这样声明:
template <typename Container, typename Index> //c现在
decltype(auto) authAndAccess(Container&& c, //是通用
Index i); //引用
在这个模板中,我们不知道我们所操作的容器是什么类型的,这意味着我们同样不知道它使用的索引对象的类型。传值给不知道类型的对象通常要冒着产生多余的拷贝的风险,对象切割的行为问题(见条款41),以及同事的嘲笑所带来的刺痛,但在容器索引的情况下,遵循STL对索引值的示例(例如,对std::vector
和std::deque
使用[]
操作)看起来是明智的,因此我们坚持传值。
但是,我们需要更新模板的实现,让它和条款25所告诫的一样,将std::forward
运用于通用引用:
template <typename Container, typename Index> //C++14
decltype(auto) //最终版
authAndAccess(Container&& c, Index i) //本
{
authenticateUser();
return std::forward<Container>(c)[i];
}
这应该可以做到任何我们想做到的,但是需要C++14编译器。如果你没有,那么你需要使用这个模板的C++11版本。它与它的C++14副本相同,除了你需要指定返回类型:
template <typename Container, typename Index> //C++11
auto //最终版
authAndAccess(Container&& c, Index, i) //本
-> decltype(std::forward<Container>(c)[i])
{
authenticateUser();
return std::forward<Container>(c)[i];
}
另一个很可能使你烦恼的问题就是该条款开头的评论,decltype
几乎总产生你期望的类型,但也会让你吃惊。老实说,除非你是大型库的维护者,否则你不太可能碰到这些问题。
为了完全理解decltype
的行为,你必须让你自己熟悉一些特殊的例子。大多数特殊情况太模糊而不能保证在这样的一本书中讨论,但是看一下这些decltype
例子就能够了解它的使用。
对一个名字使用decltype
可以得到名字的声明类型。名字是左值表达式,但这并不影响decltype
的行为。然而,对于比名字更复杂的左值表达式,decltype
确保报告的类型总是一个左值引用。也就是说,如果一个除了名字外的左值表达式的类型为T,decltype
报告的类型为T&
。这几乎没有什么影响,因为大多数的左值表达式的类型固有的包括左值引用限定符。例如:函数返回左值总是返回左值引用。
然而,这样一种行为的含义是值得我们注意的。在
int x = 0;
x
之中,x
是变量名,因此decltype(x)
的结果为int
。但将名字x
放入圆括号中—”(x)
“—会产生一个比名字复杂的表达式。作为一个名字,x
是左值,且C++中,表达式(x)
也被定义为左值。因此decltype((x))
的结果是int&
。将圆括号放在名字的周围能改变decltype
报告它的类型!
在C++11中,这只是有点奇怪,但结合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不仅返回类型与f1不同,而且还返回了局部变量的引用!这样的代码将让你乘坐向未定义行为出发的高速列车—你决不想乘的列车。
最主要的是在使用decltype(auto)
时要多留心。在报导表达式类型时,表面上看似微不足道的却可以影响decltype(auto)
报告的类型。为了确保类型推导的结果类型推导的结果是你想要的,这个技巧将在条款4描述。
同时,不能忽视全局。当然,decltype(单独的或结合auto)
可能有时在类型推导方面产生惊喜,但那不会发生在正常情况下。通常decltype
产生你所期望的类型。当对名字使用decltype
时尤其如此,因为在那种情况下,decltype
就如它所说的那样做:它报告名字的声明类型。
要记住的事:
decltype
总是产生没有任何修改的变量类型或表达式类型。- 对于类型为
T
的表达式而不是名字,decltype
总是报告T&
类型。 - C++14支持
decltype(auto)
,像auto
一样,可从其初始化器推导类型,但是使用的是decltype
类型推导规则。