条款3:理解decltype

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6

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

先从一般案例讲起——就是那些不会引发意外的案例。与模板和auto的型别推导过程相反decltyppe一般只会鹦鹉学舌。返回给定的名字或表达式的确切型别而已

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;                 //decltype(Point::x)是int
    int y;                 //decltype(Point::y)是int
};

Widget w;                  //decltype(w)是Widget
if (f(w)) ...              //decltype(f(w))是bool

template<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的规则。
阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6