选择用什么工具查看类型推导结果取决于在什么开发阶段,需要获取什么样的信息。这里将探索三种阶段:编码阶段,编译阶段和运行阶段。
通过 IDE 编辑器运行信息
在使用 IDE 编码的过程中,当你把光标放在一些程序元素附近呢(比如,变量,参数,函数等等,IDE 时常会显示出程序元素的信息,例如,看下面的代码:
const int theAnswer = 42;
auto x = theAnswer;
auto y = &theAnswer;
IDE 会提示出类似 x 的推导类型是 int,b 的推导类型是 const int*。
为了能够正常工作,代码必须是可编译状态,这样 IDE 才有可让过 C++ 编译器(或者至少是编译器前端)运行,并提供给你这部分信息。如果编译器没有足够的信息来解析类型推导,那么 IDE 是无法展示这方面信息的。
比如一个简单的类型 int,IDE 可以轻松给出类型信息。然而,随着类型的推导越来越复杂,IDE 显示出来的信息就没有那么有帮助了。
通过编译器报错信息
常见的通过编译器来确认类型的方法就是使用用一下这个类型,然后得到编译器的报错,从编译器的报错信息中可以知道错误信息以及造成它的原因。
假设,我们想知道上个例子中 x 和 y 类型的推导,首先定义一个之前没有定义过函数模版,比如下面这段代码:
template <typename T> // 这个模版定义只用于 TD
class TD; // TD 将为我们显示类型信息
如果试图实例化上面的模版,将会引发编译错误,因为没有定义模版实例。为了查看 x 和 y 的类型,可以实例化模版:
TD<decltype(x)> xType; // 产生错误信息,错误信息包含了 x,y 的类型。
TD<decltype(y)> yType;
下面的信息是我从出错信息里面筛选出来的,通过编译上面的代码,其中一个编译器给出的错误信息如下:(类型信息在尖括号内)
error: aggregate 'TD<int> xType' has incomplete type and cannot to define;
error: aggregate 'TD<const int *> xType' has incomplete type and cannot to define;
使用另一个编译器得到的信息是:
error: 'xType' uses undefined class 'TD<int>'
error: 'yType' uses undefined class 'TD<const int *>'
忽略这两种编译器提示不同的部分,我测试过的几乎所有的编译器都能给出这样有用的错误信息。
通过运行时输出
在运行时显示类型信息的方法是通过 printf 函数(这并不是我推荐的),但是这个方法能够自定义输出格式,那么关键就是组织适合的文本格式来输出必要的信息。你可能觉得不怎么香,但是有 typeid 和 std::type_info::name 的加持,就很香了。接下来就来看看怎样推导 x 和 y 的类型,你可能觉得代码应该这样写:
std::cout << typeid(x).name() << '\n';
std::cout << typeid(y).name() << '\n'; // 输出 x 和 y 的类型名称
用这种方法来得到 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” 表示 “pointer to const”,(这两个编译器都支持一个工具,c++filt,他能解析出看的懂的类型名)。微软的编译器输出的信息就比较容易理解了,x 是 “int”,y 是 “int const *”
由于得到了正确的信息,可能让你觉得已经解决问题了,先不要急,思考一个更复杂的问题:
template <typename T>
void f(const T& param); // 可以执行的模版函数
std::vector<Widget> createVec(); // 工厂方法
const auto vw = createVec(); // 通过工厂返回初始化 vw
if (!vm.empty()) {
f(&vw[0]); // 调用函数
...
}
这段代码里包含了一个用户自定义的类型(Widget),一个 STL 的容器(std::vector),和一个 auto 变量(vw),让你更深刻的了解编译器是怎样推导的。比如,理解模版 T 推导出什么类型和函数 f 的参数是什么类型就是一件很棒的事情。
现在,这段代码加上 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 的含义是 “pinter to const”(指向常量的指针),后面跟着的6是个秘密,再紧接着的字符串(Widget)就是类型名。所以编译器已经告诉你 T 和 param 的类型都是 const Widget*。
微软编译器的输出就直接多了:
T = class Widget const *
param = class Widget const *
3 种不同的编译器都输出了同样精准的信息,但是再仔细观察下,param 定义的类型是 const T&,你不觉得 T 和 param 的类型一样很奇怪吗?如果 T 是int, param 的类型应该是 const int&,和 T 的类型是不同的。
不幸的是,std::type_info::name 返回的信息并不可靠。在这个例子里,3 个编译器返回的结果都不正确,其实,本质上就需要他们是错误的,因为 std::type_info::name 的本质都是通过值传递,将类型传递给模版函数,正如 Item 1 中所描述的那样,这意味着如果类型是一个引用,则忽略引用部分,在去掉引用部分以后如果类型是 const(或者 volatile),那么这部分也将被忽略,这也就是为什么 const Widget* const & 类型最后输出的是 const Widget * 类型。
同样不幸的是,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 &
这个类型看上去没有那么恐怖,但是中间 “…” 的部分肯定会让你迷惑,直到你意识到 IDE 忽略了这部分信息。运气好的化,你的开发环境可能在这个问题上处理的更好。
如果更加倾向依靠一些库而不是赌运气的话,你会因为 std::type_info::name 和 IDE 没有给你想要而感到欣慰,Boost 的 TypeIndex 库 (常常会写成 Boost.TypeIndex)就是为这个正确答案而设计的。这不是 C++ 标准库的一部分,但它既不是 IDE 也不是 class TD 这样的东西。并且,boost 库是跨平台并且开源的(可以在 boost.org 免费得到),并且得到了世界上最严苛的公司团队的认可,这意味着 使用 Boost 代码和标准库几乎没有差别,并且可以无损跨平台移植。
下面的代码将展示 f 函数如何使用 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';
// 输出 param 的类型信息
cout << " param = " << type_id_with_cvr<decltype(param)>().pretty_name() << '\n';
...
}
这里可能得到精确类型的原因是函数模版 boost::typeindex::type_id_with_cvr 得到一个类型参数(我们想要知道的类型参数)别切没有忽略掉 const, volatile 或者是引用限定(这就是函数名中 with_cvr 的含义)。返回的结果是一个 boot::typeindex::type_indexobject,包含一个叫 pretty_name 的成员函数,这个函数产生一个 std::string 对象,包含了一个看得懂的类型信息。
通过这个函数 f 的实现,再回顾一下使用 typeid 产生的错误信息:
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 库都只是工具用来帮助你弄清楚编译器是如何推导类型的,这些方法都很有用,但总而言之,你还是需要自己理解 Item 1 -3 中那些推导类型的知识。
重点总结
- 查看类型推导的信息可以通过 IDE,编译器报错和 Boost.TypeIndex 库查询。
- 有些方法得到的类型信息可能并不精确,而且没有什么用处,因此理解 C++ 的类型推导规则是非常必要的。