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

条款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 TD
class TD;                              //TD == "Type Displayer"

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

TD<decltype(x)> xType                 //elicit errors containing
TD<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() << '
';     // display types for
std::cout << typeid(y).name() << '
';     // 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工具,来对这些重整后的名字进行解码)。理解编译器的输出将变得easy起来,Microsoft的编译器提供了更清楚的输出,x的类型是int,y的类型是int const*.

由于对x和y显示的结果是正确的,你可能会觉得问题已经攻克了。可是我们不能草率。想想以下这个更复杂的样例:

template<typename T>             // template function to
void f(const T& param);          // be called 
std::vector<Widget> createVec(); // factory function 
const auto vw = createVec();     // init vw w/factory return
if (!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() << '
';         // show T 
    cout << "param = " << typeid(param).name() << '
'; // 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__ << '
';  // Clang 
#elif defined(_MSC_VER) 
    std::cout << __FUNCSIG__ << '
';          //For Microsoft 
#endif 
… 
}

像之前那样调用f,

std::vector<Widget> createVec();  // factory function 

const auto vw = createVec();      // init vw w/factory return
if (!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。是“集成开发环境”的英文缩写,能够辅助开发程序的应用软件。

原文地址:https://www.cnblogs.com/gavanwanggw/p/7054562.html