我可以/我应该使用 std::exception 进行常规错误处理吗?

Posted

技术标签:

【中文标题】我可以/我应该使用 std::exception 进行常规错误处理吗?【英文标题】:Can I / Should I use std::exception's for regular error handling? 【发布时间】:2015-07-07 22:38:44 【问题描述】:

我打算用 C++ 开始这个新项目,并且正在考虑一种不痛苦的方法来进行错误处理。现在,我不打算开始抛出和捕获异常,而且很可能根本不会抛出异常,但我在想——即使是常规的错误处理,为什么要自己滚动/复制粘贴一个类来描述错误/状态,什么时候我可以只使用std::exception 及其子类(或者可能是std::optional<std::exception>)?

using Status = std::optional<std::exception>;
Status somethingThatMayFail(int x);

是否有人/任何项目以这种方式工作?这是一个荒谬的想法还是有点古怪?

【问题讨论】:

@vsoftco:见编辑。我说的是不抛出异常的错误处理。 有趣...我最近听说谷歌似乎在使用这样的系统,不知道是真是假。如果你的函数必须返回一些东西怎么办?你会使用一对/元组吗? @vsoftco:我们不谈细节......问题只是关于报告错误的用途。但是 - 该函数将有一个输入/输出参数(引用或指针),或者它可能会返回一些案例类(我确信 C++ 现在有类似的东西,比 C 联合更好)。 这是一个有趣的问题,我没有看到您提出的例外情况,但我期待看到您问题的答案。但是imo您在最有用的地方没有使用异常:即在RAII中,您确保通过堆栈展开调用适当的析构函数。你提出的方式在某种意义上类似于 C 的做事方式,比如返回错误代码。 您可能想查看Alexandrescu's Expected<T> 【参考方案1】:

我认为你不应该构造异常,除非你真的打算抛出它们。我会推荐一个 bool 或 enum 返回类型。阅读您的代码的人的意图会更清楚,他们会更快。但是,如果你构造了一个异常,其他人就会出现并认为他们可以抛出异常并导致整个系统崩溃。

C++ 异常在资源管理、触发析构函数等 (RAII) 中发挥着重要作用。以任何其他方式使用它们都会损害性能,并且(更重要的是)让以后试图维护代码的人感到困惑。

但是,您可以使用与 std::exception 没有任何关系的状态报告类来做您想做的事情。人们在不需要时为“更快”的代码做了太多的事情。如果状态枚举不够好,并且您需要返回更多信息,那么状态报告类将起作用。如果它使代码更容易阅读,那就去吧。

除非你真的抛出它,否则不要称它为异常。

【讨论】:

这个。即使optional&lt;exception&gt; 非常适合用例,在 throw/catch 上下文之外使用它也会令人困惑。应尽可能避免混淆。【参考方案2】:

我认为仅凭性能可能会出现问题。考虑以下代码:

#include <iostream>
#include <chrono>
#include <ctime>
#include <stdexcept>

#include <boost/optional.hpp>   


int return_code_foo(int i)   

    if(i < 10)  
        return -1;
    return 0;
 


std::logic_error return_exception_foo(int i)   

    if(i < 10)  
        return std::logic_error("error");
    return std::logic_error("");
 


boost::optional<std::logic_error> return_optional_foo(int i)   

    if(i < 10)  
        return boost::optional<std::logic_error>(std::logic_error("error"));
    return boost::optional<std::logic_error>();
 


void exception_foo(int i)   

    if(i < 10)  
        throw std::logic_error("error");
 


int main()

    std::chrono::time_point<std::chrono::system_clock> start, end;

    start = std::chrono::system_clock::now();
    for(size_t i = 11; i < 9999999; ++i)
        return_code_foo(i);
    end = std::chrono::system_clock::now();
    std::cout << "code elapsed time: " << (end - start).count() << "s\n";

    start = std::chrono::system_clock::now();
    for(size_t i = 11; i < 9999999; ++i)
        return_exception_foo(i);
    end = std::chrono::system_clock::now();
    std::cout << "exception elapsed time: " << (end - start).count() << "s\n";

    start = std::chrono::system_clock::now();
    for(size_t i = 11; i < 9999999; ++i)
        return_optional_foo(i);
    end = std::chrono::system_clock::now();
    std::cout << "optional elapsed time: " << (end - start).count() << "s\n";

    start = std::chrono::system_clock::now();
    for(size_t i = 11; i < 9999999; ++i)
        exception_foo(i);
    end = std::chrono::system_clock::now();
    std::cout << "exception elapsed time: " << (end - start).count() << "s\n";

    return 0;

在我的 CentOS 上,使用 gcc 4.7,它的时间是:

[amit@amit tmp]$ ./a.out 
code elapsed time: 39893s
exception elapsed time: 466762s
optional elapsed time: 215282s
exception elapsed time: 38436s

在原版设置中,并且:

[amit@amit tmp]$ ./a.out 
code elapsed time: 0s
exception elapsed time: 238985s
optional elapsed time: 33595s
exception elapsed time: 24350

在 -O2 设置下。

附:我个人会使用异​​常/堆栈展开,因为我相信它是 C+ 的基本部分,可能正如 @vsoftco 上面所说的那样。

【讨论】:

嗯,是的,重点是,但您测量的是实际发生错误的时间,而不是没有发生错误的时间。此外,如果我的错误处理代码使用数字状态代码将字符串传递到某处,或者将其写入流,那将等同于我认为的运行时间。 @einpoklum 实际上,我不同意-如果您检查代码,则此测试中从未发生过“异常”。这正是这种方法的问题——对于正常情况来说,它似乎非常昂贵。这也适用于您关于实际处理错误的观点 - 它往往不那么频繁。 (如果你考虑一下,yeshpomashehu :-))【参考方案3】:

回答您的问题,这不是一个荒谬的想法,您实际上可以使用std::exception 进行常规错误处理;有一些注意事项。

使用std::exception作为函数的结果

假设一个函数可以在几种错误状态下退出:

std::exception f( int i )

  if (i > 10)
    return std::out_of_range( "Index is out of range" );

  if ( can_do_f() )
    return unexpected_operation( "Can't do f in the current state" );

  return do_the_job();

您如何使用std::exception 或可选的来处理这个问题?当函数返回时,将创建异常的副本,只保留std::exception 部分并忽略实际错误的细节;给你留下的唯一信息是“是的,出了点问题……”。该行为与返回预期结果类型的布尔值或可选值(如果有)相同。

使用std::exception_ptr保存细节

另一种解决方案是应用与std::promise 相同的方法,即返回std::exception_ptr。在那里,您将能够不返回任何内容或返回异常,同时保留实际的错误详细信息。恢复错误的实际类型可能仍然很棘手。

在同一对象中返回错误或结果

最后另一种选择是使用Expected&lt;T&gt; proposal 和its implementation。在那里,您将能够在单个对象中返回值或错误,并根据需要处理错误(通过测试错误或使用常规异常处理),对于函数不返回值的情况有一些特殊性(更多Stack Overflow 或 this blog)。

如何选择

我个人对此事的看法是,如果您要使用异常,请按照设计的方式使用它们,最终使用一些额外的工具,如 Expected&lt;T&gt; 以使其更容易。否则,如果你不能使用标准的异常处理,那就去寻找一个已经证明自己的解决方案,比如经典的错误代码系统。

【讨论】:

以上是关于我可以/我应该使用 std::exception 进行常规错误处理吗?的主要内容,如果未能解决你的问题,请参考以下文章

std :: current_exception应该从类的析构函数中的catch块返回非null值

在抛出std :: exception的实例后终止调用

std::exception::_Raise 和 std::exception::exception 上的 VC++ 链接器错误

std::any 由 std::exception_ptr

如何捕获 I/O 异常(确切地说是 I/O,而不是 std::exception)

为啥 std::exception 在 VC++ 中有额外的构造函数?