C++ vs. D、Ada 和 Eiffel(带有模板的可怕错误消息)

Posted

技术标签:

【中文标题】C++ vs. D、Ada 和 Eiffel(带有模板的可怕错误消息)【英文标题】:C++ vs. D , Ada and Eiffel (horrible error messages with templates) 【发布时间】:2011-04-03 11:27:34 【问题描述】:

C++ 的一个问题是我们从大量使用模板和模板元编程的代码中得到可怕的错误消息。这些概念旨在解决这个问题,但遗憾的是它们不会出现在下一个标准中。

我想知道,这个问题对于所有支持泛型编程的语言来说都是常见的吗?还是 C++ 模板有问题?

不幸的是,我不知道任何其他支持泛型编程的语言(Java 和 C# 泛型过于简化,不如 C++ 模板强大)。

所以我问你们:D、Ada、Eiffel 模板(泛型)是否也会产生如此丑陋的错误消息?是否有可能拥有具有强大通用编程范式但没有丑陋错误消息的语言?如果是,这些语言是如何解决这个问题的?

编辑: 反对者。我真的很喜欢 C++ 和模板。我并不是说模板不好。实际上,我是通用编程和模板元编程的忠实粉丝。我只是在问为什么我会从编译器那里收到如此丑陋的错误消息。

【问题讨论】:

C++ 会有很好的错误消息,如果 TMP 是以任何方式计划的。你可以说它是“错误”地进入了语言。 STL Error Message Decryptor。使疼痛基本消失。 @Mat:如果只有积极的开发不会停止。 :( 不会回到 VC9,你知道的。 @user 如果你尝试编译 C++ 代码,它大量使用模板、boost 等。你会得到很长(几页)和丑陋的错误。 @Serge Dundich 如果一个编译器肯定会给出这样的消息,但如果所有编译器都给出这样的错误消息,这意味着语言有问题。向 C++ 添加概念的主要原因之一是帮助编译器生成更清晰的错误消息。 【参考方案1】:

总的来说,我发现泛型的 Ada 编译器错误消息实际上并不比任何其他 Ada 编译器错误消息更难阅读。

另一方面,C++ 模板错误消息因error novels 而臭名昭著。我认为主要区别在于 C++ 进行模板实例化的方式。问题是,C++ 模板比 Ada 泛型灵活得多。它非常灵活,几乎就像一个宏预处理器。 Boost 中的聪明人已经使用它来实现诸如 lambda 甚至其他语言之类的东西。

由于这种灵活性,每次第一次遇到模板参数的特定排列时,基本上都必须重新编译整个模板层次结构。因此,解决到 API 下几层不兼容的问题最终会提交给糟糕的 API 客户端进行解密。

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

是的,C++ 模板错误消息比 Ada 的要糟糕得多。

现在调试完全是另一回事了...

【讨论】:

C++ 模板是图灵完备的,因此它们不仅仅是一个宏预处理器。【参考方案2】:

问题的本质在于,无论在什么情况下,错误恢复都很困难。

当您考虑到 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 说明其含义很明显 在这种情况下,编译器会解析文件的其余部分,就好像修复程序已经存在一样,而不是在乱码行上喷出一行 (最近)避免包含用于注释的包含堆栈,以减少内容 (最近)仅尝试公开开发人员实际编写的模板参数类型,并保留 typedef(因此谈论 std::vector<Name> 而不是 std::vector<std::basic_string<char, std::allocator<char>>, std::allocator<std::basic_string<char, std::allocator<char>> >,这一切都不同了) (最近)在缺少 template 的情况下正确恢复,以防在另一个模板方法中调用模板方法时丢失它

但是每一个都需要几个小时的工作。

他们当然不是免费来的。

现在,概念应该(通常)让我们的生活更轻松。但它们大多未经测试,因此认为最好将它们从草案中删除。我必须说我为此感到高兴。鉴于 C++ 的相对惯性,最好不要包含尚未彻底修改的功能,而且概念图并没有让我真正兴奋。他们似乎也没有让 Bjarne 或 Herb 激动,因为他们说他们将从头开始重新考虑概念,以制定下一个标准。

【讨论】:

“C 认为不为函数指定返回类型是正常的(在这种情况下,它默认为 int)”。但是,C++ 绝对不允许这种语法,并且由于问题涉及 C++,我肯定会编辑它。 @DeadMG:我想不出 C++ 中歧义的一个很好的例子(模板之外),所以我现在就这样吧 :) 如果你自己有,那么编辑回答,我不介意。 C++ 特有的歧义要糟糕得多。例如。 SomeType Foo(abcd); 声明的内容不能从语法中推断出来。只有环境才能给出正确答案:如果abcd 是类型名称,那么Foo 是一个函数,它采用abcd 类型的参数,返回SomeType,如果abcd 是对象名称,那么Foo 是SomeType 类型的对象用abcd 值初始化。这是长期以来 G++ 开发人员面临的主要问题。还有许多其他类型名称与对象名称的问题。无论如何,你的信息很棒。 +1 A*B = D+E;:如果A是一个类型,那就是减速。如果A 是一个变量,它会超载operator* 以返回超载赋值的东西,那就是表达式求值。 @Serge, @BCS:是的,这些都是模棱两可的,但我正在寻找存在错误时的模棱两可(这里是一个被遗忘的分号)来说明为什么编译器有时会提供相当无用的消息.无论如何,感谢您提供的示例,它们很好地说明了语法本身的问题。【参考方案3】:

文章Generic Programming 概述了多种语言中泛型的许多优缺点,包括Ada in particular。尽管缺少模板特化,但所有Ada generic instances 都“等同于实例声明……紧跟在实例主体之后”。实际上,错误消息往往发生在编译时,它们通常代表常见的违反类型安全的行为。

【讨论】:

此外,Ada 源代码更易于阅读。这会导致更易读的错误消息。【参考方案4】:

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的错误总是很清楚? C++0x 有static_assert,所以单独的第二部分不再真正让 D 变得更好。但是结合编译时字符串处理,D 的static assert 的错误报告能力要优越得多(例如,你不能在 C++0x 中的ideone.com/TQ6wY 中显示qa**bint)。跨度> @Ashot:D 不会遇到这个问题,因为模板写得很好。它仍然提供快速而肮脏的模板。【参考方案5】:

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

因此,它不仅仅是一个简单的编译器复制和粘贴。不幸的是,用几行来解释差异是不可能的。去看看 EiffelStudio。

【讨论】:

【参考方案6】:

我们正在努力改进错误消息。例如,Clang 非常重视生成更易于阅读的编译器错误消息。我只使用了很短的时间,但与 GCC 的等效错误相比,到目前为止我对它的体验是相当积极的。

【讨论】:

以上是关于C++ vs. D、Ada 和 Eiffel(带有模板的可怕错误消息)的主要内容,如果未能解决你的问题,请参考以下文章

Ruby简介

Eiffel:有没有办法禁止使用任何特别继承的创建方法?

未定义符号:__gnat_rcheck_CE_Invalid_Data 将 ADA 与 C++ 链接时

Rubyruby安装

C++ 应用程序(调试版)包含 VS 运行时库 msvcr90.dll 和 msvcr90d.dll

VS2010 中的 C++ 嵌套 lambda 错误,带有 lambda 参数捕获?