先说说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);
但是由于对模板中操作的容器型别不清楚,对未知型别按照值传递会存在诸多风险:
- 非必要的复制操作带来性能隐患。
- 对象切割
slicing
问题带来的行为异常。 - 同行的嘲笑(这里是作者的玩笑,而且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的一个推导规则:
- decltype应用于一个名字之上,就会得出该名字的声明型别。名字其实是左值表达式。
- 如果仅有一个名字,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的规则。 |