关于c ++:断言邪恶?

Is assert evil?

Go语言创建者写道:

Go doesn't provide assertions. They are undeniably convenient, but our experience has been that programmers use them as a crutch to avoid thinking about proper error handling and reporting. Proper error handling means that servers continue operation after non-fatal errors instead of crashing. Proper error reporting means that errors are direct and to the point, saving the programmer from interpreting a large crash trace. Precise errors are particularly important when the programmer seeing the errors is not familiar with the code.

你对此有何看法?


不,只要你按计划使用,assert就没有问题。

也就是说,它应该用于在调试期间捕获"不可能发生"的情况,而不是正常的错误处理。

  • 断言:程序逻辑本身的故障。
  • 错误处理:错误的输入或系统状态,不是由程序中的错误引起的。

不,以东十一〔0〕和以东十一〔1〕都不是恶的。但两者都可能被误用。

断言用于进行健全性检查。如果不正确的话,应该杀死程序的东西。不用于验证或作为错误处理的替代。


按照这种逻辑,断点也是邪恶的。

断言应该用作调试帮助,而不是其他。"邪恶"是当你尝试使用它们而不是错误处理的时候。

断言是为了帮助你,程序员,检测和修复不存在的问题,并验证你的假设是否成立。

它们与错误处理无关,但不幸的是,有些程序员滥用它们,然后将它们声明为"邪恶"。


我很喜欢使用断言。当我第一次构建应用程序时(也许对于新的域),我发现它非常有用。我没有做非常花哨的错误检查(我会考虑提前优化),而是快速编码,并添加了很多断言。在我了解了更多关于事情如何工作的知识之后,我重写并删除一些断言,然后更改它们以更好地处理错误。

由于断言的存在,我花费了大量的时间编写/调试程序。

我还注意到断言帮助我思考许多可能破坏程序的事情。


它们应该用于检测程序中的错误。不错的用户输入。

如果使用得当,它们就不是邪恶的。


作为附加信息,go提供了一个内置函数panic。这可以用来代替assert。例如。

1
2
3
if x < 0 {
    panic("x is less than 0");
}

panic将打印堆栈跟踪,因此在某种程度上它具有assert的目的。


这就产生了很多问题,我认为一个使断言的辩护变得混乱的问题是,它们通常基于论点检查。所以考虑一下这个不同的例子,当您可能使用断言时:

1
2
3
4
5
6
7
8
9
10
11
12
13
build-sorted-list-from-user-input(input)

    throw-exception-if-bad-input(input)

    ...

    //build list using algorithm that you expect to give a sorted list

    ...

    assert(is-sorted(list))

end

您对输入使用异常,因为您希望有时会得到错误的输入。您断言对列表进行排序是为了帮助您在算法中找到错误,根据定义,这是您所不期望的。断言只在调试版本中,因此即使检查很昂贵,您也不介意在每次调用例程时都这样做。

您仍然需要对生产代码进行单元测试,但这是一种不同的、互补的方法,可以确保代码是正确的。单元测试确保您的例程符合其接口,而断言是一种更细粒度的方法,可以确保您的实现正按照您期望的方式进行。


断言并不邪恶,但很容易被滥用。我同意"断言通常被用作拐杖,以避免考虑正确的错误处理和报告"。我经常看到这个。

就我个人而言,我喜欢使用断言,因为它们记录了我在编写代码时可能做出的假设。如果在维护代码的同时破坏了这些假设,则可以在测试期间检测到问题。但是,在进行生产构建(即,使用ifdefs)时,我确实强调了从代码中去掉每个断言。通过剥离生产构建中的断言,我消除了任何人将它们作为拐杖滥用的风险。

断言还有另一个问题。断言只在运行时检查。但通常情况下,您希望执行的检查可能是在编译时执行的。最好在编译时检测一个问题。对于C++程序员来说,Boost提供了BooStyStistAsAsHyt,允许您这样做。对于C程序员,本文(链接文本)描述了一种可以在编译时执行断言的技术。

总之,我遵循的经验法则是:不要在生产构建中使用断言,如果可能的话,只对编译时无法验证的东西使用断言(即必须在运行时检查)。


我承认在考虑错误报告时使用了断言。然而,这并不意味着它们在正确使用时非常有用。

如果你想遵循"早点崩溃"的原则,它们尤其有用。例如,假设您正在实现一个引用计数机制。在代码的某些位置,您知道refcount应该是零或一。另外,假设refcount错误,程序不会立即崩溃,但是在下一个消息循环期间,很难找出出错的原因。断言将有助于检测更接近其来源的错误。


我更喜欢避免在调试和发布中执行不同操作的代码。

但是,在条件下中断调试程序并拥有所有文件/行信息非常有用,这也是精确的表达式和精确的值。

断言"只在调试中评估条件"可能是一种性能优化,因此,只有在0.0001%的程序中才有用,因为人们知道他们在做什么。在所有其他情况下,这是有害的,因为表达式可能实际上改变程序的状态:

assert(2 == ShroedingersCat.GetNumEars());会使程序在调试和发布时做不同的事情。

我们已经开发了一组断言宏,它将抛出异常,并在调试和发布版本中执行。例如,THROW_UNLESS_EQ(a, 20);会抛出一个异常,即what()消息同时具有文件、行和a的实际值等。只有宏才有这种能力。调试程序可能被配置为在特定异常类型的"throw"处中断。


我非常不喜欢断言。但我不会说他们是邪恶的。

基本上,断言和未检查的异常一样,唯一的异常是断言(通常)不应该为最终产品保留。

如果您在调试和构建系统时为自己构建了一个安全网,为什么您会拒绝为您的客户、支持服务台或任何能够使用您当前构建的软件的人提供此安全网?只对断言和异常情况使用异常。通过创建一个适当的异常层次结构,您将能够很快地分辨出其中一个。除此之外,断言仍然存在,并且可以在失败时提供有价值的信息,否则将丢失。

因此,我完全理解go的创建者,通过完全删除断言并强制程序员使用异常来处理这种情况。对此有一个简单的解释,例外只是一个更好的机制,为什么要坚持古老的断言?


我最近开始在代码中添加一些断言,我就是这样做的:

我将代码分为边界代码和内部代码。边界代码是处理用户输入、读取文件和从网络获取数据的代码。在这段代码中,我在一个循环中请求输入,该循环仅在输入有效时(在交互用户输入的情况下)退出,或者在不可恢复的文件/网络损坏数据的情况下抛出异常。

内部代码就是一切。例如,在类中设置变量的函数可以定义为

1
2
3
4
void Class::f (int value) {
    assert (value < end);
    member = value;
}

从网络获取输入的函数可能如下所示:

1
2
3
4
5
6
void Class::g (InMessage & msg) {
    int const value = msg.read_int();
    if (value >= end)
        throw InvalidServerData();
    f (value);
}

这给了我两层支票。任何在运行时确定数据的地方都会得到异常或立即的错误处理。但是,使用assert语句的Class::f中的额外检查意味着如果某些内部代码调用Class::f,我仍然有一个健全性检查。我的内部代码可能没有传递一个有效的参数(因为我可能已经从一些复杂的函数系列中计算了value),所以我喜欢在设置函数中有一个断言来记录,不管谁在调用函数,value不能大于或等于end

这似乎符合我在一些地方读到的内容,即在一个运行良好的程序中断言不可能被违反,而异常情况应该适用于仍然可能出现的异常和错误情况。因为从理论上讲,我正在验证所有输入,所以不可能触发我的断言。如果是,我的程序是错误的。


简短回答:不,我相信断言是有用的


是的,断言是邪恶的。

通常在应该使用正确错误处理的地方使用它们。从一开始就习惯于写正确的生产质量错误处理!

通常它们会妨碍编写单元测试(除非您编写了一个与测试工具交互的自定义断言)。这通常是因为在应该使用正确错误处理的地方使用它们。

大多数情况下,它们是从发布版本中编译出来的,这意味着当您运行实际发布的代码时,它们的"测试"都不可用;考虑到在多线程情况下,最糟糕的问题通常只出现在发布代码中,这可能是很糟糕的。

有时它们是破坏设计的拐杖;也就是说,代码的设计允许用户以不应该调用的方式调用它,断言"阻止"了这一点。修复设计!

我在2005年的博客上写了更多关于这个的文章,网址是:http://www.lenholgate.com/blog/2005/09/assert-is-evil.html


assert由于输入较少,因此被滥用用于错误处理。

因此,作为语言设计者,他们更应该看到,可以通过更小的输入来完成正确的错误处理。排除断言,因为您的异常机制是verbose而不是解决方案。哦,等等,Go也没有例外。太糟糕了:


当我看到这一点时,我觉得自己像是在踢作者的头。

我一直在代码中使用断言,并最终在编写更多代码时将它们全部替换。当我没有编写所需的逻辑时,我使用它们,当我遇到代码时,我希望得到警告,而不是编写一个异常,当项目接近完成时,它将被删除。

异常也更容易与我不喜欢的生产代码融合在一起。断言比throw new Exception("Some generic msg or 'pretend i am an assert'");更容易被注意到。


我对这些为断言辩护的答案的问题是,没有人清楚地说明什么使它不同于常规的致命错误,以及为什么断言不能是异常的子集。现在,有了这句话,如果这个例外永远不会被发现呢?这是否使它成为一个命名断言?而且,为什么你会想要在语言中强加一个可以引发一个异常的限制,这个异常是/没有/可以处理的?


如果您所说的断言意味着程序呕吐然后存在,那么断言可能非常糟糕。这并不是说它们总是错误的,它们是一个很容易被滥用的结构。他们还有许多更好的选择。像这样的事情是被称为邪恶的好候选人。

例如,第三方模块(或任何真正的模块)几乎不应该退出调用程序。这并不能让调用程序的程序员控制此时程序应该承担的风险。在许多情况下,数据非常重要,即使保存损坏的数据也比丢失数据要好。断言会强制您丢失数据。

断言的一些替代方法:

  • 使用调试器,
  • 控制台/数据库/其他日志记录
  • 例外情况
  • 其他类型的错误处理

参考文献:

  • HTTP//FTPNGUOR/GONGNU/MANALALS/NANA1.14/HTMLNoNO/NANAY3.HTHTML
  • HTTP://www. LeHelgGATE.COM/BLG/2005/09/AsReSt- IS-EVIL.HTML
  • GO不提供断言,并且有很好的理由:http://gangang.org/doc/Faq.*断言
  • http://c2.com/cgi/wiki?不使用断言

即使提倡断言的人也认为他们应该只用于发展,而不是用于生产:

  • http://codebetter.com/gregyong/2007/12/12/asserts-are-not-evil/
  • http://www.codeproject.com/articles/6404/assert-is-your-friend
  • http://parabellumgames.wordpress.com/using-asserts-for-debugging/

此人表示,当模块可能损坏了在引发异常后仍然存在的数据时,应使用断言:http://www.advogato.org/article/949.html。这当然是一个合理的观点,但是,外部模块不应该规定损坏的数据对调用程序有多重要(通过退出"for"它们)。处理这种情况的正确方法是抛出一个异常,使程序现在可能处于不一致的状态。而且,由于好的程序主要由模块组成(在主可执行文件中有一些粘合代码),断言几乎总是错误的。


EDCOX1 0是非常有用的,当意外出现故障时停止程序时,可以避免大量的回溯。

另一方面,很容易滥用assert

1
2
3
4
int quotient(int a, int b){
    assert(b != 0);
    return a / b;
}

正确的版本如下:

1
2
3
4
5
6
7
bool quotient(int a, int b, int &result){
    if(b == 0)
        return false;

    result = a / b;
    return true;
}

所以…从长远来看…在大局中…我必须同意可以滥用assert。我总是这么做。


与其说是邪恶,不如说是适得其反。永久性错误检查和调试之间有一个分离。断言使人们认为所有调试都应该是永久性的,并且在经常使用时会导致大量的可读性问题。永久性错误处理应该比需要的地方更好,因为断言会导致它自己的错误,这是一个相当可疑的实践。


我从不使用assert(),示例通常显示如下内容:

1
2
int* ptr = new int[10];
assert(ptr);

这很糟糕,我从来没有这样做,如果我的游戏是分配一堆怪物呢?为什么我要让游戏崩溃,而你应该优雅地处理错误,所以做如下的事情:

1
2
3
4
5
6
7
8
9
CMonster* ptrMonsters = new CMonster[10];
if(ptrMonsters == NULL) // or u could just write if(!ptrMonsters)
{
    // we failed allocating monsters. log the error e.g."Failed spawning 10 monsters".
}
else
{
    // initialize monsters.
}