Effective Modern C++ Item 3

来源:互联网 发布:java运行命令行参数 编辑:程序博客网 时间:2024/05/20 17:23

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的示例中,我们用ci指定了返回类型。如果我们使用传统的方式,在函数名之前有返回类型,ci是不可用的,因为它们还没有被声明。

使用这样的声明,正如我们期望的,当申请传入容器时,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&类型,但autoauthAndAccess的返回类型推导将去除引用,因此产生的返回类型为int。那个int,将作为函数的返回值,是个右值,上述的代码尝试将10赋值给一个int类型的右值。这在C++中是禁止的,因此代码将不会编译。

为了让authAndAccess像我们期望的一样,我们需要对返回值使用decltype类型推导,换句话说,去指定authAndAccess返回的类型与表达式c[i]的一样。C++的拥护者期望在类型推导的情况下使用decltype类型推导规则,在C++14中通过decltype(auto)说明符让这个成为了可能。最初这看起来是矛盾的(decltypeauto?),实际上可以合得来: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的类型为Widgetdecltype(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::vectorstd::deque使用[]操作)看起来是明智的,因此我们坚持传值。

但是,我们需要更新模板的实现,让它和条款25所告诫的一样,将std::forward运用于通用引用:

template <typename Container, typename Index>       //C++14decltype(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++11auto                                                //最终版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类型推导规则。
原创粉丝点击