摘要
本文是关于 decltype()
的学习笔记。各种版权都属于原作者。如果存在侵权行为,请通知本人删除。
decltype()
这是个什么东西?对于指定的名字或者表达式,decltype()
能给出它的类型,就仿佛 sizeof()
能告诉我对象的尺寸。
名字或表达式(举例) | decltype 的用法 | 结果类型 |
---|---|---|
const int i = 0; |
decltype(i) //普通变量 |
const int // 变量的类型 |
bool f(const Widget& w) |
decltype(w) //函数形参decltype(f) //函数名称 |
const Widget& //形参的类型bool(const Widget&) //函数的类型 |
struct point { int x, y; };
|
decltype(Point::x) //结构体成员decltype(Point::y)
|
int 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; ... if (v[0]==0)...
|
std::vector 的简化版 decltype(v) //模板类的实例划对象名字decltype(v[0]) // 取元素的表达式 |
vector<int> int&
|
从这张表大概可以感受到decltype()
的能力和推导规则,基本上就是有一说一。
貌似可以翻译为
拓印类型
或者叫类型拓印
,类似于中国传统的拓印技术。
最佳实践:代码案例研究
假设我们想要撰写一个函数,其形参包括一个容器,支持方括号下标语法(即[])和一个下标,并会在返回下标操作结果前进行用户验证。函数的返回值型别必须与下标操作结果的返回值型别相同。
下标操作返回的是对容器内元素的引用。
版本一 C++11 (v1.0)
template<typename Container, typename Index>
auto authAndAccess( Container& c, Index i)
-> decltype(c[i]) // 学生以为这种写法着实难看, 不是C++的传统风格。
{
authenticateUser();
return c[i];
}
这里的auto
与类型推导没有任何关系,它仅仅是C++11中的函数的返回类型尾序语法的指示(trailing return type syntax),即 返回类型放在形参列表之后(在 ->
之后)。尾序返回值的好处是可以使用形参。
版本二 C++14(有缺陷 v0.1)
template <typename Container, typename Index)
auto authAndAccess( Container &c, Index i ) // c++14
{
authenticationUser();
return c[i]; // 函数的返回类型根据 c[i] 推导,有点问题
}
auto
这里不再是个简单的指示了,而是要求编译器推导函数的返回类型,即c[i]
类型。
还有点儿问题,在哪儿呢?看一段使用它的代码:
std::deque<int> d;
// ...
authAndAccess(d, 5) = 10;
// 验证用户,并返回d[5],然后将其赋值为10(因为
// 从逻辑上函数返回的似乎是int&引用,所以使用者认为可以赋值修改),
// 但这个代码无法通过编译
原来,在模板类型推导规则中,如果是在初始化表达式中,那推导结果类型中的引用修饰就被去掉了。编译时会爆出如下的错误:
[ ~/workspace/temp ] % g++ -std=c++14 -Wall -o decltype decltype.cc
decltype.cc:20:23: error: expression is not assignable
authAndAccess(d, 5) = 10;
~~~~~~~~~~~~~~~~~~~ ^
剥掉了引用属性的返回类型是一个int
型右值,无法被赋值修改。
如果我把函数调用放到赋值的右边,并用一个引用型变量接收,看看编译器报告的错误信息:
int & v = authAndAccess(d, 5)
// ^^^^^^^^^^^^^^^^
// error: non-const lvalue reference to type 'int'
// cannot bind to a temporary of type 'int'
从消息中可以清晰看到,函数返回类型是int
,它的引用属性被干掉了。
版本三 用 C++14 实现(v1.0)
如何才能同时做到:
- 让编译器自动推导函数返回类型。
- 让编译器使用
decltype()
的推导规则(而不是auto
的推导规则)。
答案好像也很简单:鱼和熊掌一锅炖了……
template<typename Container, typename Index>
decltype(auto) // C++14, 现在函数的返回类型真的跟c[i]一样了!
authAndAccess( Container& c, Index i)
{
authenticateUser();
return c[i];
}
作者终于端出来了一盘美味!
decltype(auto)
它不光可以用在函数返回类型上,也可以用在变量上。
Widget w;
const Widget& cw = w;
auto myWidget1 = cw; // 使用 auto 类型推导规则,
// 结果myWidget1的类型是Widget
decltype(auto) myWidget2 = cw; // 亦是类型推导,但应用的是decltype()
// 结果myWidget2的类型就是 Widget&
怎么来理解 decltype(auto)
呢?总而言之就是告诉编译器这里需要做自动类型推导,并且推导规则就用 decltype()
的规则。
版本四:C++11/14 v2.0 面对右值引用的代码缺陷
缺陷与如何使用这个 authAndAccess()
时的实参有关系。
第(1)种情况:在这个函数调用表达式之前,第1个入参即容器已经存在,并且在表达式执行完毕后还继续存在。例如:
std::deque<int> d;
// ... 一些处理
auto s = authAndAccess(d, 5);
s = 10;
之前的版本处理的很好。
第(2)种情况:容器入参是在函数调用表达式执行时产生的,表达式执行完毕后对象就析构了。例如:
std::deque<std::string> makeStringDeque(); // 工厂函数
auto s = authAndAccess(makeStringDeque(), 5);
s += "ok";
第2行的函数调用表达式种,第1个参数是由工厂函数生成的一个临时对象;表达式执行完毕后,这个临时对象就析构了。像这种对临时对象的引用,一般叫右值引用。
第3行代码现在还正确吗?注意,s
现在成了一个悬空引用。为什么?当右值对象析构时,会一并析构其元素对象(即 std::string
)。
解决方法有两种:(1)利用函数重载,一个版本用于左值引用,一个版本用于右值引用。(2)使用“万能引用”并配合“完美转发”。
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(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)
要极其小心翼翼。
查看类型推导结果的方法
(一)利用 IDE 编辑器和编译器,实现静态的类型查看。
(二)利用 std::type_info::name
结果有时不太准确。。
(三)利用 Boost.TypeIndex 组件。
#include <boost/type_index.hpp>
template<typename T>
void f( const T& param )
{
using std::cout;
using boost::typeindex::type_id_with_cvr;
// 显示 T 的类型
cout << "T = "
<< type_id_with_cvr<T>().pretty_name()
<< '\n';
cout << "param = "
<< type_id_with_cvr<decltype(param)>().pretty_name()
<< '\n';
}
再看看用户端代码:
std::vector<Widget> createVec(); // 工厂函数
const auto vw = createVec(); // 使用工厂函数返回值初始化 vw
if ( !vw.empty() ) {
f(&vw[0]);
// ...
}
输出是:
T = Widget const*
param = Widget const* const&
本文内容来自多种资料,包括但不限于《Effective Modern C++》,《C++ Primer》(第5版),《C++程序设计语言》(第四版)等。
网友评论