【问题标题】:C++ throwing exceptions (need advices)C++ 抛出异常(需要建议)
【发布时间】:2016-09-03 19:15:14
【问题描述】:

我正在阅读 bjarne Stroustrup 的 The C++ Programming Language,尤其是关于异常安全和 RAII 编程习惯的章节。我熟悉 RAII,但不熟悉抛出异常。实际上,我看不到 throw 关键字经常被使用。我在研究标准模板库以了解模板机制和 RAII 习语时,很少看到它(矢量、红/黑树、...)。

如果我要使用 throw 关键字,我会倾向于一直使用它。因此,这种语法意味着使用 try-catch 子句,这会使代码看起来很丑。

您对此有何看法?是否有更好的技术来处理异常?还是我绝对应该使用 throw?

感谢您的回复。

【问题讨论】:

    标签: c++ exception-handling


    【解决方案1】:

    首先,这些链接可能会有所帮助。 Link 1 Link 2

    大多数时候,C++ 异常比任何替代方法都更适合。实际上,我认为它使代码更漂亮。我给你举个例子,希望你能理解。

    假设我必须加载一个模型,并准备在屏幕上渲染它(我知道你可能不熟悉图形编程,但你应该明白这一点)。这意味着我有一个很大的功能,或者至少有一个很大的功能可以调用其他几个较小的功能。整个过程包括打开模型文件、读取所有坐标、打开材质文件、为材质分配内存、打开纹理文件、为纹理分配内存、加载纹理等。很多东西。

    但有时,在最后,我可能会遇到无法找到纹理文件之一或无法打开它的错误。如何处理?小文件打开函数是否向纹理加载函数返回一个值,纹理加载函数检查它,意识到发生了一些不好的事情,然后又返回一些东西给读取主模型文件的函数等?你不觉得每个点的所有返回、检查和释放资源看起来都很丑吗?

    这里有一个更好的方法:将 load_model() 函数包装在 try 块中,然后在需要的地方插入一些 throw 语句。并在catch 部分中包含所有“可用内存”代码。它看起来会更干净,而且你出错的可能性也更小。

    我希望你能理解这个想法。如果您有任何问题,请向他们提问。

    【讨论】:

    • “我知道你可能不熟悉图形编程,但你应该明白这一点”。我使用 OGL 进行 3D 图形表示原型设计。无论如何,感谢您的链接,但是当我们想要使用返回大量 C 类错误代码的其他库时,编写 throw 语句会变得非常长。
    【解决方案2】:

    只有当我有一个必须返回某些东西并且我可以返回的函数时,我才使用 throw。当我返回指针时,一个空指针就足够了,但是当返回引用时,你什么都不能返回,所以我抛出了。其中一部分,我从不扔,也从不抓,因为你说的原因。

    C++ 有更好的错误处理功能。首先,在以后的 C++ 版本中会有函数契约。它将允许您这样做:

    struct Database {
        SomeResult query(std::string) [[expect: connected]] {
            // ...
        }
    
    private:
        bool connected = false;
    };
    

    而且错误处理程序是可自定义的,所以可能是异常,也可能是std::terminate


    还有std::expected 提案。这是一个您现在可以在 boost 实现中使用的实用程序。

    它允许函数要么返回结果,要么返回错误。然后,该函数的用户可以以比catch 更漂亮的方式正确处理它。考虑以下示例,取自提案:

    有了预期,我们不需要使用异常,我们可以使用 std::error_condition 比 std::exception_ptr 如果我们想使用错误。为了...的目的 这个例子,我们使用下面的枚举(样板代码 关于 std::error_condition 没有显示):

    enum class arithmetic_errc
    {
        divide_by_zero, // 9/0 == ?
        not_integer_division // 5/2 == 2.5 (which is not an integer)
    };
    

    使用expected,代码变为:

    expected<double,error_condition> safe_divide(double i, double j)
    {
        if (j==0) return make_unexpected(arithmetic_errc::divide_by_zero); // (1)
        else return i / j; // (2)
    }
    

    在提案中,它表明您可以像这样更改用户功能:

    例如,基于异常的函数 i + j/k 是:

    double f1(double i, double j, double k)
    {
        return i + safe_divide(j,k);
    }
    

    但变成使用预期:

    expected<double, error_condition> f1(double i, double j, double k)
    {
        auto q = safe_divide(j, k)
        if(q) return i + *q;
        else return q;
    }
    

    它稍后表明您也可以使用 std::expected::map 编写它的缩短版本:

    expected<double, error_condition> f1(double i, double j, double k)
    {
        return safe_divide(j, k).map([&](double q){
            return i + q;
        });
    }
    

    您可以在此处阅读这两个提案:Simple Contact for C++std::expected proposal

    【讨论】:

      【解决方案3】:

      异常非常适合处理exceptional cases

      • 阻止构造函数完成其工作的错误。
      • 预期不会发生的错误。一个典型的例子是内存分配问题
      • 可能会级联的错误,因为它们表明明显不利的情况

      但是当错误不是异常时出现异常should not be used,即预期会定期发生(例如循环直到条件发生)。在这种情况下,特殊的返回值是更好的选择。

      原因有三个:

      • 概念性:“例外”一词强烈暗示特殊情况。
      • 性能:现代编译器进入 try 块几乎没有明显的性能影响:这是一种非常便宜的保护。但是当你抛出时,显然有一个堆栈展开工作比返回一些值更昂贵(然而,“更昂贵”仍然比显示消息要快......一个数量级:大约 7µs与我的旧 i7 cpu 相比,与纳秒级别的等效回报相比
      • 健壮性:如果作为堆栈展开的一部分而被销毁的对象自身抛出异常,异常处理terminate() 您的程序会突然。这将是一个非常不可能的情况。但是,如果您滥用异常处理并在几乎正常的情况下使用它来代替返回,那么您会增加一天在这种情况下的可能性,除非you'd take extreme care

      【讨论】:

      • “但是当你抛出时,显然有一个堆栈展开工作,这比返回一些值要昂贵得多”。是的,但这并不重要。最重要的是从错误中恢复没有?
      • @Papipone 是的,我完全同意!我喜欢你的“恢复”这个词;我想说的是,有时我们称某些事情为“错误”,即使这是完全可以预料的情况并且没有什么可以恢复的。因为 OP 说 “如果我要使用 throw (...),我会倾向于一直使用它”.
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-05-27
      • 1970-01-01
      • 2012-06-28
      • 2015-02-13
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多