C++ vs. D, Ada和Eiffel(使用模板时出现可怕的错误消息)

28

C++存在一个问题,即使用模板和模板元编程时我们所得到的错误信息非常可怕。概念(concepts)是为解决这个问题而设计的,但不幸的是它们不会出现在下一个标准中。

我在想,这个问题是否普遍存在于所有支持泛型编程的语言中?或者C++模板有什么问题?

不幸的是,我不知道还有哪种语言支持泛型编程(Java和C#的泛型太简化了,没有C++模板那么强大)。

因此,我问一下各位:D、Ada和Eiffel中的模板(泛型)是否也会产生这样丑陋的错误信息?而且是否可能拥有强大的泛型编程范式,但没有丑陋的错误信息呢?如果是,这些语言是如何解决这个问题的呢?

编辑:对于那些给我投反对票的人。我真的很喜欢C++和模板。我并不是说模板不好。实际上,我是泛型编程和模板元编程的粉丝。我只是想知道为什么编译器给我返回这样丑陋的错误信息。


6
如果TMP(模板元编程)是有计划的,C++ 将会有更好的错误信息。可以说,TMP 以一种“错误”的方式悄然进入了这门语言。 - Xeo
1
STL错误信息解密器。让痛苦大部分消失。 - Mat
7
如果只有一个编译器给出这样的消息,那肯定没问题,但如果所有编译器都给出这样的错误消息,这意味着语言中出现了问题。添加C++概念的主要原因之一是帮助编译器生成更清晰的错误消息。 - UmmaGumma
1
@Ashot Martirosyan:尽管如此,大多数难看的多行错误消息来自于完整模板特化展开,而不是仅仅按代码中出现的类型名称编写。显然,这完全是编译器设计问题 - 而不是 C++ 问题。 - Serge Dundich
1
C♯和Eiffel的泛型与C++的模板不同:在Eiffel和C♯中,你会说这个方法接受一个实现了X的G。而在C++的模板中,你只需要说它接受一个类T,然后在使用时,它会尝试查看类型是否具有所需的任何方法(编译时/静态“鸭子类型”http://en.wikipedia.org/wiki/Duck_typing)。这种静态鸭子类型的方法是错误信息不清楚的原因。 - ctrl-alt-delor
显示剩余10条评论
6个回答

19

总的来说,我发现 Ada 泛型编译器的错误信息并没有比其他 Ada 编译器的错误信息更难读。

另一方面,C++ 模板错误信息因为被称为“错误小说”而臭名昭著。主要区别在于 C++ 处理模板实例化的方式。问题在于,C++ 模板比 Ada 泛型更加灵活,几乎像一个宏预处理器。Boost 中聪明的人们使用这个特性来实现诸如 Lambda 和甚至整个其他语言等功能。

由于这种灵活性,每当其特定参数的排列方式第一次遇到时,整个模板层次结构基本上都必须重新编译。因此,导致不兼容性的问题最终呈现给 API 客户端来解释。

在 Ada 中,泛型实际上是强类型的,并且向客户端提供了完全的信息隐藏,就像普通的程序包和子程序一样。因此,如果您收到错误消息,通常只涉及到您正在尝试实例化的一个泛型,而不是用于实现它的整个层次结构。

因此,是的,C++ 模板错误消息比 Ada 的更糟糕。

现在调试则是完全不同的故事...


2
C++ 模板是图灵完备的,因此它们不仅仅是一个宏预处理器。 - naasking

18

问题的本质是无论在什么上下文中,错误恢复都很困难。

当考虑到C和C++可怕的语法时,你只能想象错误消息不比这更糟糕!我担心C语法是由那些不了解语法基本属性的人设计的,其中一个属性是越少地依赖于上下文越好,另一个属性是应该尽可能使其不含歧义。

让我们举个常见的错误示例:忘记了分号。

struct CType {
  int a;
  char b;
}
foo
bar() { /**/ }

好的,所以这是错误的,缺少分号应该放在哪里?很不幸,它是模棱两可的,可以放在foo之前或之后,因为:

  • C认为在定义struct之后在步进中声明变量很正常
  • C认为在函数中不指定返回类型很正常(在这种情况下默认为int

如果我们思考一下,我们可以看到:

  • 如果foo命名的是类型,则它属于函数声明
  • 如果不是,则它可能表示一个变量......除非我们打了一个错别字并且它应该被写成fool,这恰好是一种类型:/

如您所见,错误恢复非常困难,因为我们需要推断作者想表达什么,并且语法远非易接受。虽然它不是不可能,大多数错误确实可以更或多或少地诊断出来,并且甚至可以从中恢复......只是需要相当大的努力。

似乎在开发gcc的人更感兴趣的是生产快速代码(我指的是快速,请搜索gcc 4.6上的最新基准测试),并添加有趣的功能(gcc已经实现了C++0x的大多数-如果不是全部)。而不是生产易于阅读的错误消息。你能责怪他们吗?我不能。

幸运的是,有些人认为准确的错误报告和良好的错误恢复是非常值得的目标,其中一些人已经在CLang上工作了相当长的时间,并且他们正在继续这样做。

一些不错的特性,从我的记忆中脱颖而出:

  • 简洁但完整的错误消息,其中包括源范围,以便精确定位错误的来源
  • Fix-It注释,当很明显时表示意图
  • 在这种情况下,编译器解析文件的其余部分,就像修复已经存在一样,而不是喷出一堆乱码行
  • (最近)避免在注释中包含包含堆栈,以减少垃圾信息
  • (最近)仅尝试公开开发人员实际编写的模板参数类型,并保留typedefs(因此谈论std::vector<Name>而不是std::vector<std::basic_string<char,std :: allocator<char>>,std :: allocator<std :: basic_string<char,std :: allocator<char>>>这是有很大区别的)
  • (最近)在从另一个模板方法内部调用模板方法时,在缺少template的情况下正确恢复

但是每个特性都需要几个小时到数天的工作。

它们肯定不是免费的。

现在,概念应该(通常)使我们的生活更轻松。 但它们大多未经测试,因此被认为最好从草案中删除它们。 我必须说我为此感到高兴。 鉴于C ++相对惯性,最


6
C认为不指定函数返回类型是正常的(这种情况下默认返回int)。然而,C++绝对不允许这种语法,考虑到问题涉及到C++,我肯定会将其编辑掉。 - Puppy
@DeadMG:我想不出C++中模糊性的好例子(除了模板),所以现在就这样吧 :) 如果你有自己的例子,那么请编辑答案,我不介意。 - Matthieu M.
3
C++中存在更糟糕的不确定性,例如,SomeType Foo(abcd)声明的内容无法从语法中推断出来。只有上下文才能给出正确答案:如果abcd是类型名称,则Foo是一个函数,它需要以abcd类型的参数作为输入并返回SomeType;如果abcd是对象名称,则Foo是一个类型为SomeType的对象,其用abcd的值进行初始化。这对G++开发人员长期以来是一个重大问题,还有许多其他类型名称和对象名称的问题。无论如何,你的留言很好,+1。 - Serge Dundich
3
如果A是一种类型,则这是一个声明。如果A是一个变量,它重载了operator*以返回某个重载赋值的东西,那么这就是一个表达式求值。 - BCS
@Serge, @BCS:是的,这些确实存在歧义,但我正在寻找在出现错误(这里是忘记了分号)的情况下歧义的例子,以说明为什么编译器有时会提供相当无用的消息。无论如何,感谢你们提供的例子,它们很好地说明了语法本身的问题。 - Matthieu M.

11

该文章泛型编程概述了几种语言中泛型的优缺点,特别是Ada语言中的泛型。尽管缺少模板特化,所有的Ada泛型实例“等同于紧接着实例体声明的实例声明...”。就实际情况而言,错误消息通常在编译时发生,并且它们通常代表类型安全方面熟悉的违规行为。


8
此外,Ada源代码更易读,这导致错误信息更易读。 - Rommudoh

9

D语言有两个特性可以提高模板错误消息的质量:约束和static assert

// Use constraints to only allow a function to operate on random access 
// ranges as defined in std.range.  If something that doesn't satisfy this
// is passed, the compiler will error before even trying to instantiate
// fun().
void fun(R)(R range) if(isRandomAccessRange!(R)) {
    // Do stuff.
}


// Use static assert to check a high level invariant.  If 
// the predicate is false, the error message will be 
// printed and compilation will stop before a screen 
// worth of more confusing errors are encountered.
// This function takes any number of ranges to merge sort
// and the same number of temporary buffers to merge into.
void mergeSort(R...)(R ranges) {
    static assert(R.length % 2 == 0, 
        "Must have equal number of ranges to be sorted and temporary buffers.");

    static assert(allSatisfy!(isRandomAccessRange, R), 
        "All arguments to mergeSort must be random access ranges.");

    // Implementation
}

看起来很有趣。那么D语言不会遇到这样的问题吗?而且D语言的错误信息总是清晰明了吗? - UmmaGumma
3
C++0x有static_assert,所以仅仅因为这个特性,D语言并不能算更好。但是当与编译时字符串处理相结合时,D语言的static assert错误报告能力要更加卓越(例如,在http://ideone.com/TQ6wY中,你无法像C++0x一样展示`q{a**b}`和`int`)。 - kennytm
1
@Ashot:如果模板写得好,D语言不会遇到这个问题。但是如果使用快速而粗糙的模板,仍然会出现这个问题。 - dsimcha

5

Eiffel拥有最好的错误信息,因为它拥有最好的模板系统。它完全集成到语言中,并且表现良好,因为它是唯一一个在参数中使用协变性的语言。

因此,它远不止是一个简单的编译器复制和粘贴。不幸的是,在几行中解释这种差异是不可能的。只需前往EiffelStudio查看即可。


2

有一些努力改进错误信息。例如,Clang非常注重生成更易于阅读的编译器错误信息。我只使用它很短的时间,但是与GCC的相应错误相比,我的经验迄今为止相当积极。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接