条款49:学习破解有关STL的编译器诊断信息
条款49:学习破解有关STL的编译器诊断信息
用一个特定的大小定义一个vector是完全合法的,
vector<int> v(10); // 建立一个大小为10的vector
而string在很多方面像vector,所以你可能希望可以这么做:
string s(10); // 常识建立一个大小为10的string
这不能编译。string没有带有一个int实参的构造函数。我的一个STL平台像这样告诉我那一点:
example.cpp(20): error C2664:'__thiscall std::basic_string<char, struct std::char_traits<char>,class std::allocator<char> >::std::basic_string<char, struct std::char_traits<char>,class std::allocator<char> >(const class std::allocator<char> &)': cannot convert parameter 1 from 'const int' to 'const class std::allocator<char> &' Reason: cannot convert from 'const int' to 'const class std::allocator<char> No constructor could take the source type, or constructor overload resolution was ambiguous
是不是很奇妙?消息的第一部分看起来像一只猫走过键盘,第二部分神秘地提到了一个从未在源代码中涉及的分配器,第三部分说构造函数调用是错的。当然,第三部分是准确的,但首先让我们关注于号称猫咪散步的结果上,因为当使用string时,这是你经常遇到的诊断信息的典型。
string不是一个类,它是typedef。实际上,它是这个的typedef:
basic_string<char, char_traits<char>, allocator<char> >
这是因为字符串的C++观念已经被泛化为表示带有任意字符特性(“traits”)的任意字符类型的序列并储存在以任意分配器分类的内存中。在C++里所有类似字符串的对象实际上都是basic_string模板的实例,这就是为什么当大多数编译器发出关于“程序错误使用string”的诊断信息时会涉及类型basic_string。(一些编译器很善良,在诊断信息中会使用string的名字,但大多数不会。)通常,那样的诊断信息会明确指出basic_string(以及服务助手模板char_traits和allocator)在std名字空间里,所以常常看到错误调用string会产生提及这种类型的诊断信息:
std::basic_string<char, std::char_traits<char>, std::allocator<char> >
这十分接近于上面编译器里使用的诊断信息,但不同的编译器使用这个主题的不同变体。我使用的另一个STL平台以这种方式表示string,
basic_string<char, string_char_traits<char>, __default_alloc_template<false,0> >
string_char_traits和__default_alloc_template的名字是非标准的,但是那是生活。一些STL实现背离了标准。如果你不喜欢你当前STL实现里的背离,考虑用另一个来替换它。条款50给了你可以找到可选择实现的位置的例子。
不管编译器诊断信息怎样表示string类型,把诊断信息减少到有意义的东西的技术是一样的:用文字“string”全局替换冗繁难解的basic_string。如果你使用的是命令行编译器,通常可以很容易地用一个类似sed的程序或一种脚本语言比如perl、python或ruby来完成。(你可以在Zolman的文章——《Visual C++的STL错误信息解码器》[26]——里找到一个这样的脚本的例子。)就上面的诊断信息而言,我们用string全局替换
std::basic_string<char, struct std::char_traits<char>, class std::allocator<char> >
可以得到这个:
example.cpp(20): error C2664:'__thiscall string::string(const class std::allocator<char> &)': cannot convert parameter 1 from 'const int' to const class std::allocator<char> &'
这会清楚(或至少比较清楚)地说明问题是在传给string构造函数的参数类型里,即使仍然神秘地提及allocator<char>,但比较容易使人发现不存在只带有大小的string构造函数形式。
顺便说一下,神秘地提到分配器的原因是每个标准容器都有一个只带有分配器的构造函数。就string而论,是三个可以用一个实参调用的构造函数之一,但由于某种原因,编译器指出带有分配器的那个是你试图调用的。编译器指错了,而诊断信息也令人误解。哦哟。
至于只带有分配器的构造函数,请不要使用它。那个构造函数是为了容易构造类型相同但分配器不等价的容器。通常,那是不好的,非常不好。要知道为什么,转向条款11。
现在让我们对付更富于挑战性的诊断信息。假定你正在实现一个允许用户通过昵称而不是电子邮件地址查找人的电子邮件程序。例如,这样的程序将可能使用“The Big Cheese”作为美国总统(碰巧是president@whitehouse.gov)电子邮件地址的同义词。这样的程序可以使用一个从昵称到电子邮件地址的映射,并可能提供一个成员函数showEmailAddress,显示和给定的昵称关联的电子邮件地址:
class NiftyEmailProgram { private: typedef map<string, string> NicknameMap; NicknameMap nicknames; // 从昵称到电子邮件 // 地址的映射 public: ... void showEmailAddress(const string& nickname) const; };
在showEmailAddress内部,你需要找到与一个特定的昵称关联的映射入口,所以你可能这么写:
void NiftyEmailProgram::showEmailAddress(const string& nickname) const { ... NicknameMap::iterator i = nicknames.find(nickname); if (i != nicknames. end()) ... ... }
编译器不喜欢这个,而且有好的原因,但原因不明显。为了帮助你指出它,这是一个STL平台有帮助地发出的:
example.cpp(17): error C2440: 'initializing': cannot convert from 'class std::_Tree<class std::basic_string<char, struct std::char_traits<char>,class std::allocator<char> >,struct std::pair<class std::basic_string<char, struct std::char_traits<char>,class std::allocator<char> > const .class std::basic_string<char, struct std::char_traits<char>,class std::allocator<char> > >,struct std::map<class std::basic_string<char, struct std::char_traits<char>,class std::allocator<char> >.class std::basic_string<char, struct std::char_traits<char>,class std::allocator<char> >,struct std::less<class std::basic_string<char,structstd::char_traits<char>, class std::allocator<char> > >,class std::allocator<class std::basic_string<char, struct, std::char_traits<char>,class std::allocator<char> > > >::_Kfn, struct std::less<class std::basic_string<char, struct std::char_traits<char>,class std::allocator<char> > >,class std::allocator<class std::basic_string<char, struct, std::char_traits<char>,class std::allocator<char> > > >::const_iterator' to 'class std::_Tree<class std::basic_string<char, struct std::char_traits<char>,class std::allocator<char> >,struct std::pair<class std::basic_string<char, struct std::char_traits<char>,class std::allocator<char> > const .class std::basic_string<char, struct std::char_traits<char>,class std::allocator<char> > >,struct std::map<class std::basic_string<char, struct std::char_traits<char>,class std::allocator<char> >,class std::basic_string<char, struct std::char_traits<char>,class std::allocator<char> >,struct std::less<class std::basic_string<char,structstd::char_traits<char> .class std::allocator<char> > >,class std::allocator<class std::basic_string<char,struct std::char_traits<char>,class std::allocator<char> > > >::_Kfn, struct std::less<class std::basic_string<char, struct std::char_traits<char>,class std::allocator<char> > >,class std::allocator<class std::basic_string<char, struct std::char_traits<char>,class std::allocator<char> > > >::iterator' No constructor could take the source type, or constructor overload resolution was ambiguous
有2095个字符长,这条消息看起来相当可怕,但我看过更糟的。对于这个例子我最喜欢的STL平台之一产生了一个4812个字节的诊断信息。正如你所猜测的,错误信息以外的特性是造成我喜爱它的原因。
让我们把这团乱麻减少成容易处理的东西。我们从把basic_string乱语替换成string开始。可以产生这个:
example.cpp(17): error C2440: 'initializing': cannot convert from 'class
std::_Tree<class string, struct std::pair<class string const,class string >,struct std::map<class string, class string, struct std::less<class string >,class std::allocator<class string > >::_Kfn, struct std::less<class string >,class std::allocator<class string > >::const_iterator' to 'class std::_Tree<class string, struct std::pair<class string const .class string >,struct std::map<class string, class string, struct std::less<class string >,class std::allocator<class string> >::_Kfn,struct std::less<class string >,class std::allocator<class string> >::iterator' No constructor could take the source type, or constructor overload resolution was ambiguous
好多了。现在瘦身到745个字符了,我们可以真正地开始看消息了。很可能引起我们注意的东西之一是模板std::_Tree。标准没有说过一个叫_Tree的模板,但名字中的前导下划线随后有一个大写字母唤起了我们的记忆——这样的名字是为实现而保留。这是用来实现STL一些部分的一个内部模板。
实际上,几乎所有STL实现都使用某种内在的模板来实现标准关联容器(set、multiset、map和multimap)。就像使用string的源代码通常导致诊断信息提及basic_string一样,使用标准关联容器的源代码经常会导致诊断信息提及一些内在的树模板。在这里,它叫做_Tree,但我知道的其他实现使用__tree或__rb_tree,后者反映出使用红-黑树——在STL实现中最常使用的平衡树类型。(译注:红黑树的知识可以在数据结构或算法的相关书籍里找到。)
把_Tree先放到一边,上面的消息提到了一个我们得认出的类型:std::map<class string, class string, struct std::less<class string>, class std::allocator<class string > >。这正好是我们正使用的map类型,除了显示了比较和分配器类型(我们定义map时没有指定它们)。如果我们用那个类型的typedef——NicknameMap——替换了它,错误信息将更容易明白。于是产生了这个:
example.cpp(17): error C2440: 'initializing': cannot convert from 'class std::_Tree<class string, struct std::pair<class string const, class string >,struct NicknameMap::_Kfn, struct std::less<class string >,class std::allocator<class string > >::const_iterator' to 'class std::_Tree<class string, struct std::pair<class string const ,class string >,struct NicknameMap::_Kfn, struct std::less<class string >,class std::allocator<class string > >::iterator' No constructor could take the source type, or constructor overload resolution was ambiguous
这条信息更短,但清楚得多。我们需要对_Tree做一些事情。因为_Tree是一个实现特定(implementation-specific)的模板,知道它的模板参数的意思的唯一的方法是去读源代码,而如果不必,没有理由要去翻寻实现特定的源代码。让我们试着只是用SOMETHING替换传给的_Tree的全部东西来看看我们得到什么。这是结果:
example.cpp(17): error C2440: 'initializing': cannot convert from 'class std::_Tree<SOMETHING>::const_iterator to 'class std::_Tree<SOMETHING>::iterator' No constructor could take the source type, or constructor overload resolution was ambiguous
这是我们能够处理的东西。编译器抱怨我们试图把某种const_iterator转换成iterator,一次对常数正确性的明显破坏。让我们再次看看那段讨厌的代码,我已经高亮了引起编译器怒火的那行:
class NiftyEmailProgram { private: typedef map<string, string> NicknameMap; NicknameMap nicknames; public: ... void showEmailAddress(const string& nickname) const; }; void NiftyEmailProgram::showEmailAddress(const string& nickname) const { ... NicknameMap::iterator i = nicknames.find(nickname); if (i != nicknames.end())... ... }
有意义的唯一解释是我们试图用一个从map::find返回的const_iterator初始化i(是iterator)。那好像很古怪,因为我们是在nicknames上调用find,而nicknames在非常量对象,find因此应该返回非常量iterator。
再看看。是的,nicknames被声明为一个非常量map,但showEmailAddress是一个const成员函数,而在一个const成员函数内部,类的所有非静态数据成员都变成常量!在showEmailAddress内部,nicknames是一个常量map。突然错误信息有意义了。我们试图产生一个进入我们许诺不要修改的map中的iterator。要解决这个问题,我们必须把i改为const_iterator或我们必须使showEmailAddress成为一个非const成员函数。这两个解决方案的挑战性或许比发现错误信息的意思更少。
在本条款中,我演示了用原文替换降低错误信息的复杂度,但一旦你稍微实践,多数时间里你将可以在头脑中进行替换。我不是音乐家(我连开收音机都有困难),但别人告诉我好的音乐家可以在一瞥之间视读几个小节;他们不需要看独立的音符。有经验的STL程序员发展出一项类似的技能。他们可以不假思索地在内部把比如std::basic_string<char, struct std::char_traits<char>, class std::allocator<char> >翻译为string。你也要发展这项技能,但在你能做到之前,记得你总是可以通过用更短的记忆术替换冗长的基于模板的类型名字来把编译器诊断信息降低到可以理解的东西。在许多场合,你要做的就是用你已经使用的typedef名字替换typedef展开。那是我们用NicknameMap替换std::map<class string, class string, struct std::less<class string >, class::allocator<class string > >时所做的。
这里有一些应该能帮助你理解有关STL的编译器消息的其它提示:
- 对于vector和string,迭代器有时是指针,所以如果你用迭代器犯了错误,编译器诊断信息可能会提及涉及指针类型。例如,如果你的源代码涉及vector<double>::iterator,编译器消息有时会提及double*指针。(一个值得注意的例外是当你使用来自STLport的STL实现,而且你运行在调试模式。那样的话,vector和string的迭代器干脆不是指针。对STLport和它调试模式的更多信息,转向条款50。)
- 提到back_insert_iterator、front_insert_iterator或insert_iterator的消息经常意味着你错误调用了back_inserter、front_inserter或inserter,一一对应,(back_inserter返回back_insert_iterator类型的对象,front_inserter返回front_insert_iterator类型的对象,而inserter返回insert_iterator类型的对象。关于使用这些inserter的信息,参考条款30。)如果你没有调用这些函数,你(直接或间接)调用的一些函数做了。
- 类似地,如果你得到的一条消息提及binder1st或binder2nd,你或许错误地使用了bind1st或bind2nd。(bind1st返回binder1st类型的对象,而bind2nd返回binder2nd类型的对象。)
- 输出迭代器(例如ostream_iterator、ostreambuf_iterators(参见条款29),和从back_inserter、front_inserter和inserter返回的迭代器)在赋值操作符内部做输出或插入工作,所以如果你错误使用了这些迭代器类型之一,你很可能得到一条消息,抱怨在你从未听说过的一个赋值操作符里的某个东西。为了明白我的意思,试着编译这段代码:
vector<string*> v; // 试图打印一个 copy(v.begin(), v.end(), // string*指针的容器, ostream_iterator<string>(cout, "\n")); // 被当作string对象
- 你得到一条源于STL算法实现内部的错误信息(即,源代码引发的错误在<algorithm>中),也许是你试图给那算法用的类型出错了。例如,你可能传了错误种类的迭代器。要看看这样的用法错误是怎样报告的,通过把这段代码喂给你的编译器来启发(并愉快!)自己:
list<int>::iterator i1, i2; // 把双向迭代器 sort(i1, i2); // 传给一个需要 // 随机访问迭代器的算法
- 你使用常见的STL组件比如vector、string或for_each算法,而编译器说不知道你在说什么,你也许没有#include一个需要的头文件。正如条款48的解释,这问题会降临在长期以来都可以顺利编译而刚移植到新平台的代码。