Effective Modern C++ 条款3:理解decltype

  说起decltype,这是个古灵精怪的东西。对于给定的名字或表达式,decltype能告诉你该名字或表达式的型别。一般来说,它告诉你的结果和你预测的是一样的。不过,偶尔它也会给出某个结果,让你抓耳挠腮,不得不 去参考手册或在线FAQ页面求得一些启发。

  先从一般案例讲起——就是那些不会引发意外的案例。与模板和auto的型别推导过程相反,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 {
   ...
   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>对应deoperator[]并不返回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中的返回值型别尾序语法(trailing return type syntax),即该函数的返回值型别将在形参列表之后(在“->”之后)。尾序返回值的好处在于,在指定返回值型别时可以使用函数形参。比如,在authAndAccess中,我们在指定返回值型别时就可以使用c和i。如果我们还是使用传统的返回值型别先序语法,那c和i会由于还未声明,从而无法使用。

  采用了这么一个声明形式以后,operator[]返回值是什么性别,authAndAccess的返回值就是什么型别,和我们期望的结果一致。

  

  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]推导出来
}

  条款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的规则。总而言之,我们可以这样撰写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型别推导: Widget1的型别是Widget

decltype(auto) myWidget2 = cw;  //decltype型别推导:myWidget2的型别是const Widget&

  

  再看一遍C++14版本的authAndAccess:

template<typename Container, typename Index>  
decltype<auto> authAndAccess(Container& c, Index i) ;

  容器的传递方式是对非常量的左值引用(lvalue-reference-to-non-const),因为返回该容器的某个元素的引用,就意味着允许客户对容器进行修改。不过这也意味着无法向该函数传递右值容器。右值是不能绑定到左值引用的(除非是对常量的左值引用,与本例情况不符)。

  必须承认,想authAndAccess传递右值容器属于罕见情况。一个右值容器,作为一个临时对象,一般而言会在包含了调用authAndAccess的语句结束处被析构,而这就是说,该容器中某个元素的引用(这是authAndAccess一般情况下会返回的)会在创建它的那个语句结束时被置于空悬状态。但即使如此,想authAndAccess传递一个临时对象仍然可能是合理行为。客户可能就是想要制作该临时容器的某元素的一个副本,请看下例:

std::deque<std::string> makeStringDeque();//工厂函数

//制作makeStringDeque返回的deque的第5个元素的副本
auto s = authAndAccess(makeStringDeque(),5);

  容器的传递方式是对非常量的左值引用(lvalue-reference-to-non-const),因为返回该容器的某个元素的引用,就意味着允许客户对容器进行修改。不过这也就意味着无法向该函数传递右值容器。右值是不能绑定到左值引用的(除非是对常量的左值引用,与本例情况不符)。

  如果要支持这种用法,就得修订authAndAccess的声明,以同时接受左值和右值。重载是个办法(一个重载版本声明一个左值引用形参,另一个重载版本声明一个右值引用形参),但这么一来就需要维护两个函数。避免这个后果的一个方法是让authAndAccess采用一种既能绑定到左值也能够绑定到右值的引用形参,而条款24给出了解释,说明这正是万能引用大显身手之处。这么一来,authAndAccess就可以这样声明:

template<typename Container, typename Index>  
//c现在是个万能引用了
decltype<auto> authAndAccess(Container&& c, Index i) ;

  在本模板中,我们对于操作的容器型别并不知情,同时对下标对象型别也一样不知情。对未知型别的对象采用值传递有着诸多风险:非必要的复制操作带来的性能隐患、对象截切(slicing)问题带来的行为异常(参见条款41)等,但是在容器下标这个特定问题上,遵循标准库中给出的下标值示例(例如std::string、std::vector和std::deque的operator[])应该是合理的,所以这里坚持使用了按值传递

  不过,我们需要更新该模板的实现,以使它与条款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就保证得出的型别总是左值引用。换言之,只要一个左值表达式不仅是一个型别为T的名字,他就得出一个T&型别。这种行为一般而言没有什么影响,因为绝大多数左值表达式都自带一个左值引用饰词。例如,返回左值的函数总是返回左值引用。

  但这种行为还是会导致一个值得注意的后果,请看表达式:

int x = 0;

  其中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有着返回值型别的不同,更重要的是f2返回一个局部变量的引用!这种代码会把你送上未定义行为的快车——你永远不想乘坐的那种。

  主要的教训在于,使用decltype(auto)时需要极其小心翼翼。看似是用以推导型别表达式的写法这样无关紧要的细节,却影响着decltype(auto)得出的结果。为了保证推导所得的型别和你期望的一致,请使用条款4讲述的技术。

  同事,请勿因此丢到了大局观。没错,decltype(无论是单独使用,还是和auto配合使用)时不时地会得出意外的型别推导结果,但那说到底并非正常情形。在正常情形下,decltype产生的行呗就和你期望的一致。

  以上的说法在decltype应用于名字时尤其成立,因为在那种情况下,decltype的行为可谓名副其实:它得出的就是该名字的声明型别(declared type)。

要点速记:

1、绝大多数情况下,decltype会得出变量或表达式的型别而不作任何修改。

2、对于型别为T的左值表达式,除非该表达式仅有一个名字,decltype总是得出型别T&。

3、C++14支持decltype(auto),和auto一样,它会从其初始化表达式出发来推导型别,但是它的型别推导使用的是decltype的规则。

原文地址:https://www.cnblogs.com/malinzhai/p/10585977.html