条款四:学会查看类型推导结果

Item 4: Know how to view deduced types

选择什麼工具查看类型推导,取决于软件开发过程中你想在哪个阶段显示类型推导信息。我们探究三种方案:在你编辑代码的时候获得类型推导的结果,在编译期间获得结果,在运行时获得结果。

IDE编辑器

在IDE中的代码编辑器通常可以显示程序代码中变量,函数,参数的类型,你只需要简单的把鼠标移到它们的上面,举个例子,有这样的代码中:

const int theAnswer = 42;

auto x = theAnswer;
auto y = &theAnswer;

IDE编辑器可以直接显示x推导的结果为inty推导的结果为const int*

为此,你的代码必须或多或少的处于可编译状态,因为IDE之所以能提供这些信息是因为一个C++编译器(或者至少是前端中的一个部分)运行于IDE中。如果这个编译器对你的代码不能做出有意义的分析或者推导,它就不会显示推导的结果。

对于像int这样简单的推导,IDE产生的信息通常令人很满意。正如我们将看到的,如果更复杂的类型出现时,IDE提供的信息就几乎没有什么用了。

编译器诊断

另一个获得推导结果的方法是使用编译器出错时提供的错误消息。这些错误消息无形的提到了造成我们编译错误的类型是什么。

举个例子,假如我们想看到之前那段代码中xy的类型,我们可以首先声明一个类模板但不定义。就像这样:

template<typename T>                //只对TD进行声明
class TD;                           //TD == "Type Displayer"

如果尝试实例化这个类模板就会引出一个错误消息,因为这里没有用来实例化的类模板定义。为了查看xy的类型,只需要使用它们的类型去实例化TD

TD<decltype(x)> xType;              //引出包含x和y
TD<decltype(y)> yType;              //的类型的错误消息

我使用variableNameType的结构来命名变量,因为这样它们产生的错误消息可以有助于我们查找。对于上面的代码,我的编译器产生了这样的错误信息,我取一部分贴到下面:

error: aggregate 'TD<int> xType' has incomplete type and 
        cannot be defined
error: aggregate 'TD<const int *> yType' has incomplete type and
        cannot be defined

另一个编译器也产生了一样的错误,只是格式稍微改变了一下:

error: 'xType' uses undefined class 'TD<int>'
error: 'yType' uses undefined class 'TD<const int *>'

除了格式不同外,几乎所有我测试过的编译器都产生了这样有用的错误消息。

运行时输出

使用printf的方法(并不是说我推荐你使用printf)类型信息要在运行时才会显示出来,但是它提供了一种格式化输出的方法。现在唯一的问题是对于你关心的变量使用一种优雅的文本表示。“这有什么难的,“你这样想,”这正是typeidstd::type_info::name的价值所在”。为了实现我们想要查看xy的类型的需求,你可能会这样写:

std::cout << typeid(x).name() << '\n';  //显示x和y的类型
std::cout << typeid(y).name() << '\n';

这种方法对一个对象如xy调用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 konst const“(指向常量的指针)。(这些编译器都提供一个工具c++filt,解释这些“混乱的”类型)Microsoft的编译器输出得更直白一些:对于x输出”int“对于y输出”int const *

因为对于xy来说这样的结果是正确的,你可能认为问题已经接近了,别急,考虑一个更复杂的例子:

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表示“pointer to const”,所以只有数字6对我们来说是神奇的。其实数字是类名称(Widget)的字符串长度,所以这些编译器告诉我们Tparam都是const Widget*

Microsoft的编译器也同意上述言论:

T =     class Widget const *
param = class Widget const *

三个独立的编译器都产生了相同的信息,这表明信息应该是准确的。但仔细观察一下,在模板f中,param的声明类型是const T&。难道你们不觉得Tparam类型相同很奇怪吗?比如Tintparam的类型应该是const int&而不是相同类型才对吧。

遗憾的是,事实就是这样,std::type_info::name的结果并不总是可信的,就像上面一样,三个编译器对param的报告都是错误的。此外,它们在本质上必须是这样的结果,因为std::type_info::name规范批准像传值形参一样来对待这些类型。正如Item1提到的,如果传递的是一个引用,那么引用部分(reference-ness)将被忽略,如果忽略后还具有const或者volatile,那么常量性constness或者易变性volatileness也会被忽略。那就是为什么param的类型const Widget * const &会输出为const Widget *,首先引用被忽略,然后这个指针自身的常量性constness被忽略,剩下的就是指针指向一个常量对象。

同样遗憾的是,IDE编辑器显示的类型信息也不总是可靠的,或者说不总是有用的。还是一样的例子,一个IDE编辑器可能会把T的类型显示为(我没有胡编乱造):

const
std::_Simple_types<std::_Wrap_alloc<std::_Vec_base_types<Widget,
std::allocator<Widget>>::_Alloc>::value_type>::value_type *

同样把param的类型显示为

const std::_Simple_types<...>::value_type *const &

这个比起T来说要简单一些,但是如果你不知道“...”表示编译器忽略T的部分类型那么可能你还是会产生困惑。如果你运气好点你的IDE可能表现得比这个要好一些。

比起运气如果你更倾向于依赖库,那么你会很乐意被告知,在std::type_info::name和IDE失效的地方,Boost TypeIndex库(通常写作Boost.TypeIndex)被设计成可以正常运作。这个库不是标准C++的一部分,也不是IDE或者TD这样的模板。Boost库(可在boost.com获得)是跨平台,开源,有良好的开源协议的库,这意味着使用Boost和STL一样具有高度可移植性。

这里是如何使用Boost.TypeIndex得到f的类型的代码

#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获取一个类型实参(我们想获得相应信息的那个类型),它不消除实参的constvolatile和引用修饰符(因此模板名中有“with_cvr”)。结果是一个boost::typeindex::type_index对象,它的pretty_name成员函数输出一个std::string,包含我们能看懂的类型表示。 基于这个f的实现版本,再次考虑那个使用typeid时获取param类型信息出错的调用:

std::vetor<Widget> createVec();         //工厂函数
const auto vw = createVec();            //使用工厂函数返回值初始化vw
if (!vw.empty()){
    f(&vw[0]);                          //调用f
    …
}

在GNU和Clang的编译器环境下,使用Boost.TypeIndex版本的f最后会产生下面的(准确的)输出:

T =     Widget const *
param = Widget const * const&

在Microsoft的编译器环境下,结果也是极其相似:

T =     class Widget const *
param = class Widget const * const &

这样近乎一致的结果是很不错的,但是请记住IDE,编译器错误诊断或者像Boost.TypeIndex这样的库只是用来帮助你理解编译器推导的类型是什么。它们是有用的,但是作为本章结束语我想说它们根本不能替代你对Item1-3提到的类型推导的理解。

请记住:

  • 类型推断可以从IDE看出,从编译器报错看出,从Boost TypeIndex库的使用看出
  • 这些工具可能既不准确也无帮助,所以理解C++类型推导规则才是最重要的