C++错误码、断言和异常选择的抉择 :(

6

代码链接

我曾经在两个阵营中听到(并重复)C++异常的口号。已经过了一段时间,我只是想再次把自己聚焦,这个讨论是 特定于我所链接的代码(或像容器这样的低级类),以及它们的依赖项。我曾经是一个采用防御性和错误代码的C程序员,但这是一种繁琐的做法,现在我正在更高层次的抽象中进行编程。

因此,我正在重写一个容器类(以及它的依赖项),使其更加灵活和易读(迭代器目前不存在)。正如您所看到的,在我知道将在调用站点测试它们的情况下,我返回枚举错误代码。这些容器用于运行时构建AST,初始化并使其只读。异常存在是为了防止容器被天真地使用(可能是我未来的自己)。

在这个类中,我到处都有异常,它们让我感到不舒服。我欣赏它们的用例。如果我有选择,我可能会完全关闭它们(Boost经常使用异常,我正在构建Boost,是的,我知道它们可以被禁用,但在罗马时......)。我有将它们替换为错误代码的选择,但是嘿,我不会测试它们,那么有什么意义呢?

我应该用ASSERTS替换它们吗?人们所说的这种膨胀是什么[1] [2] [3]?每个函数调用站点都会得到额外的机器吗?还是只有那些有catch子句的函数调用站点?既然我不会捕获这些异常,那么我不应该成为这种膨胀的受害者,对吧?ASSERTS不会进入发布版本,在基本原始类(即容器)的上下文中,这是否重要?我的意思是逻辑错误会以多大的几率进入最终构建中?

由于我们喜欢回答专注的问题,以下是我的问题:你会怎么做,为什么? :D

无关链接:错误代码和它们在异常中的附带作用。

编辑2:在这种情况下,选择是ASSERTS和异常之间的选择,我认为异常是最有意义的,正如我上面提到的,容器在初始化后是只读的,大多数异常都在初始化期间触发。


1
我不想阅读364行代码。你能告诉我们相关的行号吗? - Andrew Shepherd
@mark boost-system拥有平台中立的error_code命名空间分区,而boost-exception允许您在异常中搭载任意数据。我之前在一些代码中使用过这种方法,效果很好。 - Hassan Syed
@mark 我找到了代码。我添加了一个链接展示它如何组合在一起。你需要实例化你的error_code类型。它在/src目录的某个翻译单元中 :D - Hassan Syed
4
异常、断言和错误代码都有不同的作用,不能相互替换。当你知道一个条件必须为真且它只可能由于逻辑错误而为假时,请使用断言。断言意味着“这里有一个漏洞,请修复它”。异常表示一种异常情况,不应该在预期的正常工作流中出现(例如“文件未找到”或“内存不足”)。错误代码是滥用本应用于其他目的的返回值以传达状态信息的一种方式。 - Kerrek SB
@AndrewShepherd:谢谢 :-) 我没有仔细阅读问题,所以我不觉得有资格发布答案! - Kerrek SB
显示剩余4条评论
4个回答

13

非常简单。避免使用类似于火灾的错误代码,而使用异常,除非在特定情况下错误代码确实更有意义。为什么?因为异常可以携带更多信息-例如,请参见Boost.Exception。因为它们自动传播,所以您不会犯“不检查错误条件”的错误。因为有时候你不得不(退出构造函数),所以为什么不要保持一致呢?C ++根本没有提供更好的方法来表示错误条件。

另一方面,断言用于完全不同的目的-验证代码的内部状态和应始终成立的假设。失败的断言始终是错误-而异常可能表示无效的外部输入,例如。

至于链接指南:忘记Google样式指南甚至存在,它只是可怕的,并且这不仅是我的观点。LLVM-可执行文件大小几乎无关紧要,这不是您应该浪费时间思考的事情。Qt-Qt在异常安全方面存在缺陷,但这并不意味着您的代码也必须如此。使用现代实践,保持异常安全应该不难。


我认为外部输出(运行时)与由于软件组件的不正确使用/编排而引起的错误是解决我不确定性的主要问题 :) - Hassan Syed

4
为了找到适合您的解决方案,请参考以下陈述,并从多项选择中选择一个。
这是我用来做{任何事情}的库。
- 它将被{我,有源代码访问权限的志愿者,不懂行的同事,支付我支持合同费用的人}使用。 - 如果{被误用,配置错误,存在漏洞,依赖的系统失败},它将{默默地崩溃,在调试模式下报告诊断信息,以难以忽略的方式报告问题}。 - 如果我的代码失败了,我会{永远不知道,不在意,感到难过,失去金钱,被起诉}。 - 如果使用我的代码的代码失败了,我会{永远不知道,不在意,感到难过,失去金钱,被起诉}。 - 使用我的代码的正确代码变得更加复杂是{不可接受,不是我的问题,有趣的}。 - 我认为{成本高达10倍,10%,10个时钟周期}对于所有这些来说都太高了。 - 我正在使用来自{1977年,1997年,2007年,思维实验}的编译器。
对于最常见的答案,将会有类似于每个异常3个断言和每个错误返回100个异常的代码。不合理的答案当然是从以其他方式编写的代码中反向工程而来。

我喜欢你的思路 :) - Hassan Syed
+1 对于多选题的方法(当然还有选择答案!) :) - Bojan Komazec

1
template<class errorcode>
struct ForceCheckError {
    typedef errorcode codetype;
    codetype code;
    mutable bool checked;
    ForceCheckError() 
    : checked(true) {}
    ForceCheckError(codetype err) 
    : code(err), checked(false) {}
    ForceCheckError(const ForceCheckError& err) 
    : code(err.code), checked(false) {err.checked = true;}
    ~ForceCheckError() { assert(checked); }
    ForceCheckError& operator=(ForceCheckError& err) { 
        assert(checked); 
        code=err.code; 
        checked=false; 
        err.checked=true; 
        return *this;
    }
    operator codetype&() const {checked=true; return code;}
};    
//and in case they get defined recursively (probably via decltype)...
template<class errorcode>
struct ForceCheckError<ForceCheckError<errorcode> > 
    :public ForceCheckError<errorcode>
{
    ForceCheckError() 
    : checked(true) {}
    ForceCheckError(codetype err) 
    : code(err), checked(false) {}
    ForceCheckError(const ForceCheckError& err) 
    : code(err.code), checked(false) {err.checked = true;}
};

我以前从未尝试过,但如果您喜欢错误代码并希望保证它们被检查,则可能会很方便。

ASSERTS 应该测试必须为真的事情,如果为假,则程序必须立即停止。它们不应成为异常与返回代码之争的因素。

如果启用了异常,则会在后台创建代码以使对象在堆栈展开期间正确析构。如果您没有使用异常构建(这是非标准的),则编译器可以发出该代码。(通常每个函数调用多一个操作符和每个返回多一个操作符,这几乎没有什么影响)那种额外的“膨胀”将存在于每个可能必须传播异常的函数中。因此,除了具有 nothrowthrow() 或不进行函数调用且不抛出异常的函数之外,所有函数都适用。

另一方面,除非通过像上面的辅助类强制执行,否则没有人会检查返回值。


1
如果(!checked),而不是(if checked == false)。此外,从析构函数中抛出异常是一个非常糟糕的想法——想象一下在作用域中有两个此类型的对象,并且您没有检查任何一个——第一个对象的析构函数抛出异常,触发解旋,销毁第二个对象,然后抛出异常,最终导致std::terminate。 - Cat Plus Plus
我曾经有一个类似的辅助类...我添加了一个“成功”状态[默认构造],即使未检查,也不会抛出异常。 Cat++在析构函数中对异常的警告是我不使用它的主要原因...我考虑过添加一个unchecked_exception检查,但还有其他一些特殊情况让整个事情变得麻烦,所以我从来没有去理会。 - Dennis Zickefoose
不知道为什么我之前没有改变这个,现在将throw改成了assert。@DennisZickefoose:我绝对不想要那个。错误代码可能是“无错误”,但调用者必须检查一下。 - Mooing Duck

1

这是我的观点:

我应该用ASSERTS替换它们吗?

如果客户端误用接口或存在内部/状态错误,使用断言来验证程序和状态的正确性,并防止客户端误用程序是很好的。如果您在发布中禁用了断言并希望在此之后抛出异常,那么可以这样做。或者,您可以将该断言行为添加到您抛出的异常的构造函数中。

既然我不会捕获这些异常,我就不应该成为这种膨胀的受害者了,对吧?

我使用启用了异常的应用程序(默认情况下已禁用)构建了一个应用程序。大小从37 MB增加到44 MB。这是19%的增长,而且唯一使用异常的代码在std中。该程序的主要部分没有捕获或抛出异常(毕竟,它可以在未启用异常的情况下构建)。因此,即使您不编写throwcatch,您的程序大小也会增加。

对于一些程序来说,这并不是问题。如果你使用的库是为了错误处理而设计的(或者更糟),那么在禁用它们的情况下,你真的无法使用它们。
“...逻辑错误会在最终构建中出现的可能性有多高?”
这取决于程序的复杂性。对于一个具有中等到高级别错误检查的非平凡程序,很容易在野外触发断言(即使它是假阳性)。
“既然我们喜欢回答专注的问题,那么我的问题是:你会怎么做,为什么?:D”
将使用异常的程序改为使用错误代码是一种痛苦的经历。至少,我会在现有程序中添加断言。
当然有一些复杂的程序不需要异常,但为什么你的程序不需要呢?如果你想不出一个好的答案,那么你应该继续在适当的地方编写异常,并添加断言以验证一致性并确保客户端不会误用程序。
如果你确实有一个好的理由禁用异常,那么你很可能需要大量重写代码,并且你需要一些形式的错误处理 - 无论是使用错误码、日志记录还是其他更复杂的方式都取决于你自己。

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