每月Bug++:Koenig查找Bug Jeff Claar 通常本专栏讨论未记录在案的编译器bug,但有时我认为,探讨一些业已作为问题而被记录下来但可能并非是众人皆知的常识,也是值得的。本月关于参数相关的名字查找的bug就属于这种情形。这个名字查找方法指的是,根据传递给函数的参数的类型所在的名字空间而决定调用哪一个函数。作为一个例子,让我们看一看程序清单1: 程序清单1: 一段合法的C++代码,展示了参数相关的(Koenig)查找 // 参数相关的名字查找。 通常来说,如果希望调用名字空间N中的函数,我们应该将名字空间放在函数前面明确予以指定,例如N::func(mc)。然而C++标准规定,对于没有明确指定名字空间的情形,可以考虑其他名字空间,特别是正被传给函数的参数的类型所在的名字空间。对于我们刚刚展示的例子而言,因为MyClass被声明于名字空间N中,编译器将不但在全局名字空间中查找,还会到名字空间N中去查找,因此,程序清单1中的代码的确是合法的C++代码,并且函数调用将被解析为N::func。(最起码应该如此。稍后我将描述这段代码和Visual C++存在的问题) 这种类型的查找被称为参数相关的名字查找或者“Koenig查找”,它以Andrew Koenig名字命名,是他最初提议了这一点。毋庸置疑,这是一项有用的东西,并且是一个使用标准库必不可少的条件。作为一个例子,让我们考虑std::string类(std::string其实是std::basic_string<char, char_traits<char>, allocator<char> >的一个typedef,不过眼下让我们假定它本身就是一个类)。请看下面的代码: void foo() 很明显,“s1 + s2”调用string加法运算符。然而,这个运算符实际上并没有定义为string类的成员函数,相反,它是一个单独的函数。Visual C++ 7.0将其声明于std名字空间中,如下所示: template<class _Elem, (请记住,在此种情形下,string是一个对basic_string<_Elem, _Traits, _Alloc>的typedef)因此,上面的代码和下面的代码是一样的: void foo() 然而请注意,在第一个代码片断中,我并没有指明加法运算符位于std名字空间中,但是代码通过了编译并正确执行了,因此编译器必然找到了加法运算符,这说明Koenig查找规则发挥作用了。由于s1和s2都是string,并且因为string类被声明于std名字空间中,因此编译器将会到该名字空间中寻找加法运算符。假如没有Koenig查找规则,我将不得不明确指定加法运算符存在于std名字空间中:或者将“using namespace std”放到我的代码中,或者如先前所示指明“s1 = std::operator+(s1, s2)”,或者使用如下所示丑陋得难以置信的(同时也是非法的)代码: s1 = s1 std::+ s2; // 唔! 顺带一提,此类查找只有当函数名字未经资格限定时才被施用(非资格限定名字是指那些被调用时没有明确指明名字空间的名字)。如果指定了一个名字空间,那么Koenig查找规则将不再被施用,编译器将只在要求的名字空间中进行查找即可。参数类型所在的名字空间中的函数与全局名字空间中的函数将被一视同仁。因此,像程序清单2中所示的代码实际上是非法的,这段代码在名字空间N和全局名字空间中各声明了func一次: 程序清单2: 一段非法的C++代码,展示了因Koenig查找规则而导致的模棱两可的函数调用。 // 非法的代码。 namespace N 有了Koenig查找规则,当试图决定调用哪一个函数时,编译器既会考虑func(...),也会考虑N::func(...),由于它们带有同样的参数,因此这个调用实际上是模棱两可的。事实上,Borland C++为之报出如下错误信息: Error E2015: Ambiguity between 'N::func(N::MyClass)' and 'func(N::MyClass)' in function main() Bug Visual C++ 7.0可以正确地编译string加法运算符,因此它必然支持Koenig查找,不是吗?没错,这的确有几分道理,正如读者Zeeshan Amjad指出的那样。然而实际上它仅仅支持运算符的情况而不支持其他函数的情况,因此程序清单1中展示的代码在Visual C++中实际上无法通过编译,相反,它报出如下出错信息: error C2065: 'func' : undeclared identifier 这并非是一件多大的事儿,你只要将函数调用限定以名字空间名字即可,然而程序清单2中蕴含了一个更为严肃的问题:这段代码实际上是非法的,但是由于VC++没有为之施用Koenig查找规则,因此它只看见了全局名字空间中的func(...),而对于N::func(...)则不予考虑,所以它“成功地”编译了这段代码! 这个问题已经被微软记录于VC++文档的“Standard Compliance Issues in Visual C++”一节,但是,正如本文开头所说的那样,我认为此问题大得足以值得做一期专栏。我问了我们在微软的正式联系人Jeff Peil,他说这个bug将会在“即将来临的版本”中得到修复。究竟那意味着Visual Studio 的一个service pack还是下一个主要版本,让我们拭目以待吧。 异常和转换 From: Luis Pista Hi, 在使用Visual C++ 6.0(打了SP4)时,我发现,当异常类型是一个私有派生于另外一个类的类时,抛出异常时有一个bug。当运行生成的可执行程序试图抛出你自己的异常时,将会引发一个Microsoft C++ Exception。 如果你基于程序清单3中的代码而生成一个exe文件,当“throw B(1);”语句被执行时,会引发一个Microsoft C++ Exception(在Visual Studio输出窗口(output window)中,我可以看到这样的一条消息:First-chance exception in bug.exe (KERNEL32.DLL): 0xE06D7363: Microsoft C++ Exception.)。 程序清单3: 私有继承带来的异常处理问题 // except.cpp 我已经搜索了微软的站点,但没有发现任何关于这个bug的报告。 Best Regards, Luis Pista 谢谢你的来信,Luis。在这个例子中,编译器的表现实际上是正确的。这儿的问题缘自这么一个事实:类B私有派生于类A(实际上,如果将派生修改为公有的(public),代码将会如Luis预期的那样工作)。当产生异常时,代码将去查找一个可以处理给定类型的异常的catch区块。由于异常类型为B,而B派生于A,并且catch区块将捕获类型为A&异常,你可能以为catch区块将会处理抛出的异常,然而请看看下面当抛出异常时所执行的伪码: throw B(1); 此处我省略了不少东西,尤其是堆栈展开方面的东西,但它们对于这个讨论无足轻重。问题的关键在于程序内部试图执行类似于如下的代码: B b(1); 如果B公有派生于A,这将完美工作,问题是,A对于B来说是私有的,因此这个转换是不允许的(在B的成员函数内部这是允许的,但本例的情况并非如此),所以,异常处理器(exception handler)没有被施用,从而异常被传播到函数之外。由于再也没有什么别的catch区块了,因此程序将因一个未处理的异常而被终止。 假如编译器能够捕捉此类信息那该多好啊,然而现实是它办不到。在编译期,编译器无从知道某处(比方说,在另外一个函数里)是否存在一个可以正确捕获这个异常的catch区块: void foo() 这个例子中的异常被捕获了,然而函数bar()甚至可能位于另外一个C++模块,因此对于编译器而言,确实不可能决定一个异常是否被捕获过了。 结语 正如我在本文开头所讲的那样,通常我并不讨论厂商已经记录在案的编译器bug,然而,由于Koenig查找规则是语言中如此重要的一个组成部分,我认为提一提什么地方没有实现它,是值得的。 荣耀 |