《Effective Modern C++》翻译--条款4:了解如何查看推导出的类型

来源:互联网 发布:闹钟软件 编辑:程序博客网 时间:2024/05/21 22:21

条款4:了解如何查看推导出的类型

那些想要了解编译器如何推导出的类型的人通常分为两个阵营。第一种阵营是实用主义者。他们的动力通常来自于编写程序过程中(例如他们还在调试解决中),他们利用编译器进行寻找,并相信这个能帮他们找到问题的根源。第二种是经验主义者,他们正在探索条款1-3所描述的推导规则。并且从大量的推导情景中确认他们预测的结果(“对于这段代码,我认为推导出的类型将会是…”),但是有时候,他们只是想简单的回答如果这样,会怎么样呢之类的问题?他们可能想知道如果我用一个universal reference(见条款26)替代一个左值的常量形参(例如在函数的参数列表中用T&&替代const T&)模板类型推导的结果会改变吗?

不管你属于哪一个阵营(二者都是合理的),你所要使用的工具取决于你想要在软件开发的哪一个阶段知道编译器推导出的结果。我们会阐述3种可行的方法:在编辑代码的时获得推导的类型,在编译时获得推导的类型,在运行时获得推导的类型。

IDE编辑器

当你在IDE中的编辑代码时,在你将鼠标悬停在程序实体(例如变量,参数,函数等等)上的时候,编译器显示他们的类型。例如,在下面的代码中,

const int theAnswer = 42 ;auto x = theAnswer;auto y = &theAnswer;

IDE编辑器很可能会显示出x的类型是int,y的类型是const int*。

对于这项工作,你的代码不能过于复杂,因为是IDE内部的C++编译器让IDE提供了这一项信息。如果编译器不能充分理解并解析你的代码,产生类型推导的结果,它就无法给你显示类型推导的结果。

编译器的诊断

一个有效的得知编译器对某一类型推导出的结果方法是让它产生一个编译期的错误。因为错误的报告信息肯定会提到引起错误的类型。

假如,我们想要知道上一个代码中的x和y被推导出的类型。我们首先声明一个类模板,但是不定义它,代码会像下面这样:

template<typename T>                   //declaration only for TDclass TD;                              //TD == "Type Displayer"

试图实例化这个模板会产生一个错误信息,因为没有模板的定义和实例。为了要查看x和y的类型,仅仅需要用它们的类型实例化TD:

TD<decltype(x)> xType                 //elicit errors containingTD<decltype(y)> yType                 //x's and y's types;                                      //see Item 3 for decltype info

我使用这种形式的变量名: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)显示类型的信息不能在程序运行时期使用,但是它需要对输出格式的完全控制。难点是如何让变量的类型能以文本的方式合理的表现出来,你可能会觉得“没有问题”typeid和std::type_info::name会解决这个问题的。你认为我们可以写下下面的代码来知道x和y 的类型:

std::cout << typeid(x).name() << '\n';     // display types forstd::cout << typeid(y).name() << '\n';     // x and y

这个方法依赖于typeid作用于一个对象上时,返回类型为std::type_info这一个事实,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*.

因为对x和y显示的结果是正确的,你可能会认为问题已经解决了,但是我们不能草率。想想下面这个更复杂的例子:

template<typename T>             // template function tovoid f(const T& param);          // be called std::vector<Widget> createVec(); // factory function const auto vw = createVec();     // init vw w/factory returnif (!vw.empty()) { f(&vw[0]);                       // call f}

当你想知道编译器推导出的类型是什么的时候,这段代码更具有代表性,因为它牵涉到了一个用户自定义类型widget,一个std容器std::vector,一个auto变量,例如,你可能想知道模板参数T的类型,和函数参数f的类型。

使用typeid看起来是非常直接的方法,仅仅是在f中对你想知道的类型加上一些代码:

template<typename T> void f(const T& param) {     using std::cout;     cout << "T = " << typeid(T).name() << '\n';         // show T     cout << "param = " << typeid(param).name() << '\n'; // show param's type...  }

GNU和Clang的执行结果是下面这样:

T = PK6Widget param = PK6Widget

我们已经知道PK意味着pointer to const,而6代表了类的名字中有多少个字母(Widget),所以这两个编译器告诉了我们T和param的类型都是const Widget*

微软的编译器提供了下面的结果

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

这三个编译器都提供了一样的信息,这或许暗示了结果应该是准确的,但是让我们看的更细致一点,在模板f中,param的类型被声明为constT&,既然如此的话,param和T的类型一样难道不让人感到奇怪吗,如果T的类型是int,param的类型应该是const int&,看,一点都不一样。

令人悲哀的是std::type_info::name的结果并不是可依赖的,在这个例子中,三个编译器对于param的结果都是不正确的,此外,它们必须是错误的,因为标准(specification)规定被std::type_info::name处理的类型是被按照按值传递给模板对待的,像条款1解释的那样,这意味着如果类型本身是一个引用的话,引用部分是被忽略掉的,如果引用去掉之后还含有const,常量性也将被忽略掉,,这就是为什么const Widget* const &的类型被显示为const Widget*,首先类型的引用部分被忽略了,接着结果的常量性也被忽略了。

同样令人伤心的是,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的那么吓人了,中间的…只是意味着IDE告诉你,我将T的类型显示用…替代了。

我的理解是大多数显示在这里的东西是由于typedef造成的,一旦你通过typedef来获得潜在的类型信息,你会得到你所寻找的,但需要做一些工作来消除IDE最初显示出的一些类型,幸运的话, 你的IDE编辑器会对这种代码处理的更好。

在我的经验中,使用编译器的错误诊断信息来知道变量被推导出的类型是相对可靠的方法,利用修订之后的函数模板f来实例化只是声明的模板TD,修订之后的f看起来像下面这样

template<typename T> void f(const T& param) {     TD<T> TType;                   // elicit errors containing     TD<decltype(param)> paramType; // T's and param's types     … }

GNU,Clang和Microsoft的编译器都提供了带有T和param正确类型的错误信息,当时显示的格式各有不同,例如在GUN中(格式经过了一点轻微的修改)

error: 'TD<const Widget *> TType' has incomplete type error: 'TD<const Widget * const &> paramType' has incomplete type

除了typeid

如果你想要在运行时获得更正确的推导类型是什么,我们已经知道typeid并不是一个可靠的方法,一个可行的方法是自己实现一套机制来完成从一个类型到它的表示的映射,概念上这并不困难,你只需要利用type trait和模板元编程的方法来将一个完整类型拆分开(使用std::is_const,std::is_ponter,std::is_lvalue_reference之类的type trait),你还需要自己完成类型的每一部分的字符串表示(尽管你依旧需要typeid和std::type_info::name来产生用户自定义格式的字符串表达)。

如果你经常需要使用这个方法,并且认为花费在调试,文档,维护上的努力是值得的,那么这是一个合理的方法(If you’d use such a facility often enough to justify the effort needed to write, debug,document, and maintain it, that’s a reasonable approach),但是如果你更喜欢那些移植性不是很强的但是能轻易实现并且提供的结果比typeid更好的代码的, 你需要注意到很多编译器都提供了语言的扩展来产生一个函数签名的字符串表达,包括从模板中实例化的函数,模板和模板参数的类型。

例如,GNU和Clang都支持PRETTY_FUNCTION,Microsoft支持了FUNCSIG,他们代表了一个变量(在 GNU和Clang中)或是一个宏(在Microsoft中),如果我们将模板f这么实现的话

template<typename T> void f(const T& param) {#if defined(__GNUC__)                          //For GNU and     std::cout << __PRETTY_FUNCTION__ << '\n';  // Clang #elif defined(_MSC_VER)     std::cout << __FUNCSIG__ << '\n';          //For Microsoft #endif … }

像之前那样调用f,

std::vector<Widget> createVec();  // factory function const auto vw = createVec();      // init vw w/factory returnif (!vw.empty()) { f(&vw[0]);                        //call f ...}

在GNU中我们得到了以下的结果

void f(const T&) [with T = const Widget*]

告诉我们T的类型被推导为const Widget*(和我们用typeid得到的结果一样,但是前面没有PK的编码和类名前面的6),同时它也告诉我们f参数类型是const T&,如果我们按照这个格式扩展T,我们得到f的类型是const Widget * const&,和typeid的答案不同,但是和使用未定义的模板,产生的错误诊断信息中的类型信息一致,所以它是正确的。

Microsoft的 FUNCSIG提供了以下的输出:

void __cdecl f<const classWidget*>(const class Widget *const &)

尖括号里的类型是T被推导的类型,为const Widget*。和我们用typeid得到的结果一样。括号内的类型是函数参数的类型,是const Widget* const&,和我们用typeid得到的结果不一样,但同样和我们使用TD在编译期得到的类型信息一致。

Clang的PRETTY_FUNCTION,尽管使用了和GNU一样的名字,但是格式却和GNU或是Microsoft的不一样:

void f(const Widget *const &)

它直接显示出了参数的类型,但是需要我们自己去推导出T的类型被推导为了const Widget*(或者我们也可以通过typeid来获得T的类型)

IDE编辑器,编译器的错误诊断信息,typeid和PRETTY_FUNCTION,FUNCSIG之类的语言扩展仅仅只是帮助你弄明白编译器推导出的结果是什么。但是最后,没有什么能替代条款1-3中所描述的类型推导相关的推导规则。

请记住:

•可以通过使用IDE编译器、编译错误信息、typeid、PRETTY_FUNCTIONFUNCSIG这样的语言扩展等,查看类型推导。

•一些工具提供的类型推导结果可能既没有用也不准确,所以理解C++类型推导的原则十分必要。

==============================================================
译者注释:
IDE 即Integrated Development Environment,是“集成开发环境”的英文缩写,可以辅助开发程序的应用软件。

1 0
原创粉丝点击