Effective Modern C++翻译(5)-条款4

条款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 = 42;
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[0]); // 调用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[0]); //调用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++的类型推导规则依旧很必要。
时间: 2024-10-25 08:16:34

Effective Modern C++翻译(5)-条款4的相关文章

Effective Modern C++翻译(2)-条款1

第一章 类型推导 C++98有一套单一的类型推导的规则:用来推导函数模板,C++11轻微的修改了这些规则并且增加了两个,一个用于auto,一个用于decltype,接着C++14扩展了auto和decltype可以使用的语境,类型推导的普遍应用将程序员从必须拼写那些显然的,多余的类型的暴政中解放了出来,它使得C++开发的软件更有弹性,因为在某处改变一个类型会自动的通过类型推导传播到其他的地方.   然而,它可能使产生的代码更难观察,因为编译器推导出的类型可能不像我们想的那样显而易见.   想要在

Effective Modern C++翻译(3)-条款2

条款2 明白auto类型推导 如果你已经读完了条款1中有关模板类型推导的内容,那么你几乎已经知道了所有关于auto类型推导的事情,因为除了一个古怪的例外,auto的类型推导规则和模板的类型推导规则是一样的,但是为什么会这样呢?模板的类型推导涉及了模板,函数和参数,但是auto的类型推导却没有涉及其中的任何一个. 这确实是对的,但这无关紧要,在auto类型推导和template之间存在一个直接的映射,可以逐字逐句的将一个转化为另外一个. 在条款1中,模板类型推导是以下面的模板形式进行举例讲解的:

Effective Modern C++翻译(7)-条款6:当auto推导出意外的类型时,使用显式的类型初始化语义

条款6:当auto推导出意外的类型时,使用显式的类型初始化语义 条款5解释了使用auto来声明变量比使用精确的类型声明多了了很多的技术优势,但有的时候,当你想要zag的时候,auto可能会推导出了zig.例如,我有一个函数,它以const Widget&作为参数,并且返回std::vector<bool>,每一个bool暗示了Widget是否提供了一个特殊的特性. std::vector<bool> features(const Widget& w); 进一步假设第

Effective Modern C++翻译(4)-条款3

条款3 了解decltype decltype是一个有趣的东西,给它一个变量名或是一个表达式,decltype会告诉你这个变量名或是这个表达式的类型,通常,告诉你的结果和你预测的是一样的,但是偶尔的结果也会让你挠头思考,开始找一些参考资料进行研究,或是在网上寻找答案.   我们从典型的例子开始,因为它的结果都是在我们预料之中的,和模板类型推导与auto类型推导相比(参见条款1和条款2),decltype几乎总是总是返回变量名或是表达式的类型而不会进行任何的修改 const int i = 0;

决定干点事儿--翻译一下《effective modern c++》

写了很多关于C++11的博客,总是觉得不踏实,很多东西都是东拼西凑.市场上也很少有C++11的优秀书籍,但幸运的是Meyers老爷子并没有闲赋,为我们带来了<effective modern c++>. 我们都要认清,一个人很难超越自我,超越自我的巅峰之作.因为不同的时代,也会早就不同的伟大作品. 说上面这段话的意思就是,我们不能期待<effective modern c++>能达到<effective c++>给我们带来的惊喜,但是也是出自大师之手. Learn ho

Effective Modern C++ 读书笔记 Item 1

最近发现了<Effective Modern C++>这本书,作者正是大名鼎鼎的Scott Meyers——<Effective C++>.<Effective STL>的作者. 而就在C++11逐渐普及,甚至是C++14的新特性也进入大家的视野的时候,<Effective Modern C++>一书应运而生.此书与其前辈一样,通过数十个条款来展开,只不过这次是集中于C++11和C++14的新特性.auto.decltype.move.lambda表达式……

[C++11] Effective Modern C++ 读书笔记

本文记录了我读Effective Modern C++时自己的一些理解和心得. item1:模板类型推导 1)reference属性不能通过传值参数传入模板函数.这就意味着如果模板函数需要一个reference类型的参数,必须在模板声明中将其声明为reference,否则,即使使用一个reference类型的变量调用模板函数,类型推导的结果将不带reference属性. 2)constant和volatile属性也不能通过传值参数传入模板函数,但是可以通过reference参数传入这些属性. 3

Effective C++_笔记_条款08_别让异常逃离析构函数

(整理自Effctive C++,转载请注明.整理者:华科小涛@http://www.cnblogs.com/hust-ghtao/) C++并不禁止析构函数吐出异常,但它不鼓励你这样做.考虑如下代码: 1: class Widget{ 2: public: 3: ... 4: ~Widget() {...} //假设这个可能吐出一个异常 5: }; 6:  7: void doSomething() 8: { 9: vector<Widget> v ; //v在这里被自动销毁 10: ...

Effective C++_笔记_条款12_复制对象时勿忘其每一个成分

(整理自Effctive C++,转载请注明.整理者:华科小涛@http://www.cnblogs.com/hust-ghtao/) 编译器会在必要时候为我们的classes创建copying函数,这些“编译器生成版”的行为:将被烤对象的所有成员变量都做一份拷贝. 如果你声明自己的copying函数,意思就是告诉编译器你并不喜欢缺省实现中的某些行为.编译器仿佛被冒犯似的,会以一种奇怪的方式回敬:当你的实现代码几乎必然出错时却不告诉你.所以自己实现copying函数时,请遵循一条规则:如果你为c