Item4:知道如何查看推导类型
查看类型推导结果的工具的选择取决于你想要在软件开发过程中那个阶段查看结果。我们将探究三种可能性:当你编辑代码时得到类型推导信息,在编译期间得到类型推导信息,在运行时得到类型推导信息。
IDE编辑器
当你想做某些事像是把鼠标悬停在实体上时,IDE中的代码编辑器总是显示程序实体(比如:变量,参数,函数等等)的类型。例如,给你这些代码,
const int theAnswer = 42;
auto x = theAnswer;
auto y = &theAnswer;
一个代码编辑器将可能显示x
的推导类型为int
类型,y
的是const int*
类型。
为了让其工作,你的代码必须在差不多可编译的状态,因为使IDE提供信息成为可能的是C++编译器(或至少是一个前端)在IDE内部运行。如果那个编译器不能理解你的代码来解析它且完成类型推导,它也不会向你显示什么类型被推导。
对于简单的类型像int
,从IDE中来的信息通常没问题。但是,正如我们将看到的,当涉及更复杂的类型时,通过IDE显示的就可能不是那么有用了。
编译器诊断
一种获取编译器显示其所推导的类型的有效方法就是以导致编译问题的方式来使用该类型。所报告该问题的错误消息肯定会提到引用该问题的类型。
例如,我们想要看之前例子中的x
和y
的推导类型。首先我们要声明一个类模板,但不定义它。就像下面这种奇妙的做法:
template <typename T> //只对TD声明,
class TD; //TD == "Type Displayer"
尝试去实例化这个模板就会抛出一个错误信息,因为没有模板定义来实例化。为了查看x
和y
的类型,只是尝试使用他们的类型来实例化TD
:
TD<decltype(x)> xType; //抛出错误信息包含
TD<decltype(y)> yType; //x和y的类型
我使用variableNameType
形式的变量名,因为这有助于产生那些我们能寻找到想要信息的错误信息。对于上面的代码,我的一个编译给出了部分的诊断记录:(我已经对我们所关注的类型信息进行了高亮标注):
//int被高亮
error: aggregate 'TD<int> xType' has incomplete type and
cannot be defined
//const int *被高亮
error: aggregate 'TD<const int *> yType' has incomplete type
不同的编译器会以不同形式提供相同的信息:
//int被高亮
error: 'xType' uses undefined class 'TD<int>'
//const int *被高亮
error: 'yType' uses undefined class 'TD<const int *>
除了格式不同外,当使用这个技术时,所有我测试过的编译器都产生带着有用的类型信息的错误信息。
运行时输出
直到运行期才可以使用printf
方法来显示类型信息(并非我推荐你使用printf
),但它提供了对输出格式的完全控制。挑战在于创建表示你所在乎的那些适用于显示的文本。“这轻而易举”,你会这样想,“可使用typeid
和 std::type_info
来解决。”在我们连续不断的对x
和y
类型推导进行探索的过程中,你可能会估计到我们会这样写:
std::cout << typeid(x).name() << '\n'; //显示对x和y
std::cout << typeid(y).name() << '\n'; //的类型
这种方法依靠这样一个事实,对一个对象比如x
或y
调用typeid
产生一个std::type_info
对象,而std::type_info
有一个成员函数,name
,可以产生表示名字类型的C风格字符串(换句话说,const char*
)。对std::type_info::name
的调用不保证返回任何有意义的内容,但这个实现会尽量的提供帮助。提供帮助的水平是不同的。例如:GNU和Clang编译器报告x
的类型为“i
”,y
的类型为“PKi
”。一旦你了解到,在这些编译器的输出中,“i
”的意思是“int
”且“PK
”的意思是”指向konst
const
。”(两个编译器都支持一个工具,c++ filt
,将这些“破碎”的类型解码。)微软的编译器产生比较清楚的输出:x
是“int
”,y
是“int const*
”。因为这些结果对x
和y
的类型来说是正确的,你可能试图将查看报告的类型的问题看做已经解决了,但我们不能匆忙的下决定。考虑一个更复杂的例子:
template <typename T> //模板函数
void f(const T& param); //被调用
std::vector<Widget> createVec(); //工厂函数
const auto vw = createVec(); //使用工厂函数的返回
//初始化vw
if(!vw.empty()) {
f(&vw[0]); //调用f
...
}
这个代码,涉及了用户自定义类型(Widget
),一个STL容器(std::vector
),和一个auto
变量(vw
),更能代表你可能想要了解的编译推导类型的情况,例如,这将很好的了解模板类型参数T
和在f
中函数param
所推导出的类型。
我们很容易地发现了没有typeid
。只要添加一些代码给f
就可以显示你想要查看的类型:
template <typename T>
void f(const T& param)
{
using std::cout;
cout << "T = " << typeid(T).name() << '\n'; //显示T的类型
cout << "param = " << typeid(param).name() << '\n' //显示param的类型
...
}
执行由GNU编译器和Clang编译器生成的文件会产生这样的输出:
T = PK6Widget
param = PK6Widget
对于这些编译器,我们已经知道,PK
表示“指向const
”,因此唯一难以理解的就是数字6
。这仅仅是后面类名(Widget
)字符的数量。因此这些编译器告诉我们T
和param
的类型都是const Widget*
。
微软的编译器的结果就和上面一致:
T = class Widget const *
param = class Widget const *
三个独立的编译器产生了相同的信息,说明这个信息是准确的。但让我们更深入的看。在模板f
中,param
的声明类型为const T&
。在这种情况下,T
和param
有同样的类型是不是看起来很奇怪?例如,如果T
是int
类型,param
的类型应该是const int&
—而不是完全相同的类型。
不幸的是,std::type_info::name
的结果是不可靠的。例如在这个情况中,所有三个编译器报告的param
类型都是不正确的。而且,他们本来就是错的,因为std::type_info::name
要求将类型视为以值参数传入模板函数。正如条款1所解释的,这意味着如果类型是个引用类型,则reference-ness
将被忽略,如果移除引用后const
(或volatile
),则其constness
(或volatileness
)同样将被忽略。这就是为什么param
的类型—const Widget * const &
—被报告为const Widget*
。首先类型的reference-ness
被移除了,然后指针本身的constness
被除去了。
同样不幸的,由IDE编辑器显示的类型信息也不可靠—至少不是那么使用的。对于同样的例子,一个我知道的IDE编辑器将T
的类型报告为(这个不是我编的):
const
std::_Simple_types<std::_Wrap_alloc<std::_Vec_base_types<Widget,
std::allocator<Widget> >::_Alloc>::value_type>::value_type *
同样的IDE显示param
的类型:
const std::_Simple_types<...>::value_type *const &
这个没有T
的类型吓人,但是在中间的“...
”会使你困惑直到你认识到这是IDE编辑器说明“我省略了T
类型的部分”的方式。运气好的话,你的开发环境对待像这样的代码会做的更好。
如果你更倾向于依赖库而不是运气,你就会很乐意的知道std::type_info::name
和IDE显示类型可能失败,但Boost TypeIndex库(经常写成Boost.TypeIndex)是被设计于成功显示类型的。这个库不是标准C++的一部分,也不是IDE或像TD
一样的模板。而且,Boost库(可在boost.org下载使用)事实上是一个跨平台,开源的,且是在许可下的甚至是最偏执的团队也可以愉快的使用,这意味着在代码中使用Boost库几乎和使用标准库一样有好的可移植性。
下面是函数f
如何使用Boost.TypeIndex来产生准确的类型信息:
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';
//显示param的类型
cout << "param = "
<< type_id_with_cvr<decltype(param)>().pretty_name()
<< '\n';
...
}
这个代码的工作方式是模板std::typeindex::type_id_with_cvr
接受一个类型参数(我们想要知道的类型),且不会移除const
,volatile
,或引用限定符(因此,在模板中有“with_cvr
”)。结果是得到一个boost::typeindex::type_index
对象,它的pretty_name
成员函数产生包含一个人性化表示类型的std::string
。
对于使用这样实现的f
,再次考虑当typeip
使用时,对param
产生不正确信息的调用:
std::vector<Widget> createVec(); //工厂函数
const auto vw = createVec(); //使用工厂函数的返回
//初始化vw
if(!vw.empty()) {
f(&vw[0]); //调用f
...
}
在使用GNU和Clang编译器下,Boost.TypeIndex产生这样(准确)的输出:
T = Widget const*
param = Widget const* const&
使用微软的编译器下也基本相同:
T = class Widget const *
param = class Widget const * const &
这样的结果几乎统一是很好的,但很重要的是要记住IDE编辑器,编译器错误信息,像Boost.TypeIndex一样的库只不过是帮助你计算你的编译器所推导出的类型的工具。所有这些都很有用,但说到底,这些都不能代替在条款1-3中对类型推导信息对的理解。
要记住的事:
- 推导类型的结果经常可以使用IDE编辑器,编译器错误信息和Boost TypeIndex库来查看。
- 某些工具的结果可能既没有用也不准确,因此理解C++的类型推导规则仍是重要的。