Effective Modern C++ Item 3 理解decltype

先说说decltype这个关键词的常规运行方式

const int i = 0;                //decltype(i) 是 const int
bool f(const Widget& w);        //decltype(w) 是 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&

一般来说operator[]会返回T&,只有std::vector<bool> ,并不返回bool&,而返回一个全新对象。这一点需要注意,同时在vector< bool >中有解释。这一点特殊在后续一样会引起很多奇怪的问题,需要格外小心。Item 6中会对此现象进一步详细讲解。

举个使用的例子

template<typename Container, typename Index>                //能运作,但亟需改进
auto authAndAccess(Container& c, Index i)->decltype(c[i])
{
    authenticateUser();
    return c[i];
}

这里使用了一个C++11的特性,返回值型别尾序语法,C++11允许对单表达式的lambda返回值型别进行推导,C++14把这个规则扩张到允许了一切lambda和一切函数,包括多表达式的。那么上述代码段在C++14中展现的形式会是如下这样:

template<typename Container, typename Index>                //C++14,不太正确
auto authAndAccess(Container& c, Index i)
{
    authenticateUser();
    return c[i];                                            //返回值型别是根据c[i]推导出来的
}

为什么说这里不太正确呢,考虑这样一种情况:

std::deque<int> d;
...
authAndAccess(d, 5) = 10;           //验证用户,并返回d[5],
                                    //然后将其值赋值为10,但是这段代码无法通过编译

这是因为authAndAccess(d, 5)的返回值被推导为int,推导过程是d[5]的返回值是int&,但是在返回值中的auto采用的推导方式会剥离引用,这样一来返回值类型就成了int。而作为函数的返回值,这是一个右值,将10赋值给一个右值int,这是被禁止的行为,所以代码没有办法通过编译了。
为了解决这个问题,那么就出现了最为困惑的表达方式了decltype(auto)

template<typename Container, typename Index>                //C++14,能够运行,但还是可以优化
decltype(auto) authAndAccess(Container& c, Index i)
{
    authenticateUser();
    return c[i];
}

其实这里应该这么理解,这里看上去自相矛盾,但是其实合情合理:

auto指定的了欲实施推导的型别,而推导过程中采用的是decltype的规则。

类似的decltype(auto)的用法,在下面的情况下也挺好用:

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

那么回到刚刚的问题,为什么说这个模板还可以优化呢,因为这个模板目前无法在下面的用法中使用:

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

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

这里authAndAccess第一个传参接受了一个右值,但是接受的是一个非常量的左值引用。如果需要这个模板既能接受左值和右值,有两种方式。一种是重载,写一个左值引用形参版本,写一个右值引用形参版本。另一种方法是使用万能引用。

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

但是由于对模板中操作的容器型别不清楚,对未知型别按照值传递会存在诸多风险:

  1. 非必要的复制操作带来性能隐患。
  2. 对象切割slicing问题带来的行为异常。
  3. 同行的嘲笑(这里是作者的玩笑,而且Soctt这家伙很喜欢写这个,本着原味,还是保留下来吧。)

那么完美的解决方案,还是有的:

template<typename Container, typename Index>                //C++14,最终版本
decltype(auto) authAndAccess(Container&& c, Index i)
{
    authenticateUser();
    return std::forward<Container>(c)[i];
}
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的一个推导规则:

  1. decltype应用于一个名字之上,就会得出该名字的声明型别。名字其实是左值表达式。
  2. 如果仅有一个名字,decltype的行为保持不变。如果是比仅有名字更复杂的左值表达式的话,decltype就保证得出的型别总是左值引用。

例如:

int x = 0;
//decltype(x) 的结果是int
//decltype((x)) 的结果是int&,因为要满足上述第二条规则。

这一点在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函数会返回一个函数局部变量的引用传给函数外部调用者。这是一件多么可怕的事情。

要点速记
1. 绝大多数情况下,decltype会得出变量或者表达式的型别而不做任何修改
2. 对于型别为T的左值表达式,除非该表达式仅有一个名字,decltype总是得出型别T&
3. C++14支持decltype(auto),和auto一样,它会从其初始化表达式出发来推导型别,但是它的型别推导使用的是decltype的规则。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值