Effective Modern C++翻译(5)-条款4:了解如何观察推导出的类型

时间:2022-04-10 15:06:03

条款4:了解如何观察推导出的类型

那些想要知道编译器推导出的类型的人通常分为两种,第一种是实用主义者,他们的动力通常来自于软件产生的问题(例如他们还在调试解决中),他们利用编译器进行寻找,并相信这个能帮他们找到问题的源头(they’re looking for insights into compilation that can help them identify the source of the problem.)。另一种是经验主义者,他们探索条款1-3所描述的推导规则,并且从大量的推导情景中确认他们预测的结果(对于这段代码,我认为推导出的类型将会是…),但是有时候,他们只是想简单的回答如果这样,会怎么样呢之类的问题?他们可能想知道如果我用一个万能引用(见条款26)替代一个左值的常量形参(例如在函数的参数列表中用T&&替代const T&)模板类型推导的结果会改变吗?

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

IDE编辑器

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

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

IDE编辑器很可能显示出x的类型是int,y的类型是const 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的leix
// decltype的用法可以参看条款3

我使用这种形式的变量名:variableNameType,因为:它们趋向于产生足够有用的错误信息(I use variable names of the form variableNameType, because they tend to yield quite informative error messages.)对于上面的代码,其中一个编译器的错误诊断信息如下所示(我突出了我们想要的类型推导结果)

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会解决这个问题的,你认为我们可以写下下面的代码来知道x和y 的类型:

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

这个方法依赖于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> // 被调用的
void f(const T& param); // 函数模板
std::vector<Widget> createVec(); // 工厂函数
const auto vw = createVec(); // 用工厂函数来实例化vw
if (!vw.empty()) {
f(&vw[]); // 调用f
}

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

使用typeid看起来是非常直接的方法(Loosing typeid on the problem is straightforward.),仅仅是在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),所以这两个编译器告诉了我们T和param的类型都是const Widget*

Morcrosoft的编译器提供了下面的结果

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的类型显示用…替代了。

template<typename T>

void f(const T& param)

{

TD<T> TType; // elicit errors containing

TD<decltype(param)> paramType; // T's and param's types

}

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

(My understanding is that most of what’s displayed here is typedef cruft and that

once you push through the typedefs to get to the underlying type information,

you get what you’re looking for, but having to do that work pretty much eliminates

any utility the display of the types in the IDE originally promised. With any luck,

your IDE editor does a better job on code like this.)

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

template<typename T>
void f(const T& param)
{
TD<T> TType; // 引起错误的信息包括了
TD<decltype(param)> paramType; //T和param的类型
}

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__) //对于GNU和
std::cout << __PRETTY_FUNCTION__ << '\n'; // Clang
#elif defined(_MSC_VER)
std::cout << __FUNCSIG__ << '\n'; //对于Microsoft
#endif

}

像之前那样调用f

std::vector<Widget> createVec(); // 工厂函数
const auto vw = createVec(); // 用工厂函数来实例化vw
if (!vw.empty()) {
f(&vw[]); //调用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和_PRETTU_FUNCTION_,_FUNCSIG_之类的语言扩展。
  • 这些结果可能既不是十分有用也不是那么精确,所以明白C++的类型推导规则依旧很必要。