在 C++ 非空函数中使用 throw 替换 return

Using throw to replace return in C++ non-void functions

在 C++ 函数中,将 return 替换为 throw 是一种好的做法吗?比如我有下面的代码

// return indices of two numbers whose sum is equal to target
vector<int> twoSum(vector<int>& nums, int target) {
    for(int i=0; i<nums.size()-1; ++i)
        for(int j=i+1; j<nums.size(); ++j)
        {
            if(nums[i] + nums[j] == target) return vector<int>{i, j};
        }
    // return vector<int>{};
    throw "no solution";
}

上面的代码使用我的 GCC 7.2 编译。

returnthrow 有两个不同的用途,不应被视为可以互换。当您有一个有效的结果要发送回调用者时,使用 return。另一方面,当出现某些异常行为时使用 throw。通过使用标准库中的函数并注意他们何时抛出异常,您可以了解其他程序员如何使用 throw

In C++ functions, is it a good practice to replace return with throw?

Return一般来说不是可以用throw代替的东西.

在您无事可做的特殊情况下 return,抛出异常可能是退出函数的有效方式。

是不是"good practice",是什么情况"exceptional"都是主观的。例如,对于像您这样的搜索功能,可能没有解决方案并不奇怪,我认为抛出是不合适的。

除了投掷,通常还有其他选择。将您的算法与 std::string::find 之类的算法进行比较,其中 return 是子字符串开头的索引。如果子字符串不存在,则 return 为 "non-value" std::string::npos。您可以执行相同的操作,并在未找到结果时决定对索引 -1 进行 returned。在现有表示中的 none 可以保留用于此目的的情况下,还有一种将非值表示添加到类型的通用方法:std::optional.

P.S。向量可能不是 return 一对数字的好选择。 std::pair 可能更好,或者自定义 class 如果您有合适的数字名称。

你提到的不是好的编程习惯。用 throw 替换 return 语句在生产级代码中是不可接受的,尤其是对于生成所有异常列表作为证明或反驳某些功能的方式的自动化测试平台。在设计代码时,您必须考虑软件测试人员的需求。 throw 根本不能与 return 互换。 throw 用于表示程序 错误 由于某些意外现象而发生。 return 用于表示方法完成。通常使用 return 来传输错误代码,但是 return 值不会像 throw 那样导致程序中断。此外,如果程序处理不当,throw 有权终止程序。

本质上,当在方法中检测到重大错误时使用 throw 是一种很好的做法,但如果未检测到此类错误,则应始终使用干净的 return。 可以你做那个替换吗?也许,也许它会在逻辑上起作用……但这不是好的做法,当然也不是一个流行的实现。

除了其他答案之外,还有 性能:与if 条款。

(当然,我们谈论的是微秒......这是否相关取决于您的具体用例。)

此答案的概念取自 Bjarne Stroustrup 的 C++ 编程语言。

简答

是的,异常抛出可以用作 returning 值方法。下面是一个二叉树搜索函数的例子:

void fnd(Tree∗ p, const string& s)
{
    if (s == p−>str) throw p; // found s
    if (p−>left) fnd(p−>left,s);
    if (p−>right) fnd(p−>right,s);
}


Tree∗ find(Tree∗ p, const string& s)
{
    try {
       fnd(p,s);
    }
    catch (Tree∗ q) {
        // q->str==s
        return q;
    }
    return 0;
}

但是,应该避免,因为:

  • 它们允许您将错误代码与 "ordinary code" 分开,使您的程序更具可读性、易理解性和可管理性。如果您将它们用作 return 方法,则不再适用。
  • 可能效率低下,因为异常实现依赖于将它们用作错误处理方法的假设。

除此之外,还有其他限制:

  • 异常必须是可复制类型
  • 异常只能处理同步事件
  • 在时间紧迫的系统中应避免使用它们
  • 在大型旧程序中应该避免使用它们,在这些程序中资源管理是一团糟(自由存储使用裸指针、新闻和删除进行非系统管理),而不是依赖于某些系统方案,例如资源句柄(字符串向量) ).

更长的答案

异常是抛出的对象,表示错误的发生。它可以是任何可以复制的类型,但强烈建议仅使用为此目的专门定义的用户定义类型。异常允许程序员明确地将错误处理代码与 "ordinary code" 分开,从而使程序更具可读性。

首先,异常用于管理同步事件,而不是异步事件。这是第一个限制。

人们可能会认为异常处理机制只是另一种控制结构,一种 return 将值传递给调用者的替代方法。

这有一些魅力,但应该避免,因为它可能会导致混乱和效率低下。 Stroustrup 建议:

When at all possible stick to the "exception handling is an error handling" view. When this is done code is separated into two categories: ordinary code and error handling code. This makes the code more comprehensible. Furthermore, the implementations of the exception mechanisms are optimized based on the assumption that this simple model underlies the use of the exception.

所以基本上应该避免对 return 值使用例外,因为

  • 假设它们用于错误处理而不是 returning 值 异常实现已优化,因此它们可能效率低下;
  • 它们允许将 错误代码 普通代码 分开,使代码更具可读性和可理解性。 任何有助于保持错误及其处理方式的清晰模型的东西都应该被珍惜

有些程序由于实际或历史原因不能使用异常(既不能作为错误处理,更不用说):

  • 嵌入式系统的时间关键组件,必须保证在指定的最长时间内完成操作。如果没有可以准确估计异常从 throw 传播到 catch 的最长时间的工具,则必须使用其他错误处理方法。
  • 一个大型的旧程序,其中资源管理是一团糟(免费存储使用裸指针,newsdelete 进行非系统管理),而不是依赖于某些系统方案,例如资源句柄 (strings vectors).

以上情况优先采用传统的预异常方法

不,throw 不是 return 的良好语义替代。您只想在您的代码完成了它不应该做的事情时使用 throw,而不是表示函数的完全有效的否定结果。

作为一般规则,异常意味着在执行代码期间发生异常或意外情况。看看你的函数的目的,传递的向量中没有两个整数的出现总和 target 是函数的一个很可能的结果,所以结果不是异常的,因此不应该被视为异常。

函数在无法满足其后置条件时应抛出异常。 (有些函数在不满足其先决条件时也可能抛出异常;这是一个不同的主题,我不会在这里讨论。)因此,如果您的函数 must return a求和到给定目标的向量中的一对整数,那么它别无选择,只能在找不到时抛出异常。但是如果函数的契约允许它 return 一个值来表明它无法找到这样的一对,那么它不应该抛出异常,因为它有能力履行契约。

你的情况:

  • 当函数 return 找不到两个与给定目标相加的整数时,您可以将其设为空向量。然后,它永远不需要抛出异常。
  • 或者,您可以使函数 return std::optional<std::pair<int, int>>。它永远不需要抛出异常,因为当它找不到合适的对时它可以 return 一个空的可选值。
  • 但是,如果您将其设置为 return std::pair<int, int>,那么它应该抛出异常,因为当找不到合适的对时 return 没有合理的值.

通常,C++ 程序员更喜欢编写不需要引发异常的函数,以便仅报告 "disappointments",例如搜索失败,这些很容易在本地预料和处理。 returning 值而不是抛出异常的优点在其他地方被广泛讨论,所以我不会在这里重复讨论。

因此,通过抛出异常来声明"failure"通常仅限于以下情况:

  • 该函数是一个构造函数,它根本无法建立其 class 的不变量。它必须抛出异常以确保调用代码不会看到损坏的对象。
  • 出现了一个 "exceptional" 条件,应该在比调用者更高的级别上进行处理。来电者可能不知道如何从这种情况中恢复过来。在这种情况下,异常机制的使用使调用者不必弄清楚异常情况发生时如何进行。

您的问题与语言无关。

Return 和 Throw 有不同的目的。

  • Return 用于 returning 一个值;同时,
  • throw是为了抛出异常;和,
    • 在真正异常的情况下应该抛出异常。

奖励内容(来自 Steve McConnell 的代码完成 2)

当您遇到无法正常 return 的情况时,以下是您可用的所有替代方案:

  1. Return 一个中性值 比如 -1 来自一个应该 return 计数某物的方法;
  2. Return最接近的合法值比如倒车时汽车数字速度表上的0;
  3. Return 与上一次调用相同的值 就像温度计上的温度读数,当你每秒都在读数时,你知道读数在这种情况下不会有太大差异一个短暂的间隔;
  4. Return 下一条有效数据 就像当您从 table 读取行并且特定行已损坏时;
  5. Set/Return 错误 status/code/object 在 C++ 库调用中很常见;
  6. 在警告框中显示一条消息,但注意不要泄露太多可以帮助恶意行为者的信息;
  7. 记录到文件;
  8. 抛出一个异常 就像你正在思考的那样;
  9. 如果您的系统是这样设计的,请调用中央错误handler/routine
  10. 关机.

此外,您不需要只选择上述选项之一。您可以进行组合,例如,记录到文件 向用户显示消息。

你应该采取什么方法是正确性与稳健性的问题。您希望您的程序绝对正确并在遇到错误情况时关闭,还是您希望您的程序健壮并在某个点未能遵循所需路径时继续执行?

仅仅因为该函数在最后一次调用时抛出异常,并不意味着它正在替换 return。这只是逻辑的流程。

问题不应该是:

is it a good practice to replace return with throw?

相反,它应该是关于:如何定义您的 API 和合同

如果您想向函数的用户保证 vector 永远不会为空,那么抛出异常是个好主意。

如果您想保证您的函数不会抛出异常,而不是 return在某些情况下抛出一个空 vector,那么抛出是个坏主意。

在这两种情况下,用户都必须知道他们必须采取什么行动才能正确使用您的功能。

我认为问return是否可以替换[=45]是错误的问题=] 通过 抛出 returnthrow 正在做两个非常不同的事情:

  • return 用于return 函数的常规结果。
  • throw 是在结果值不存在时对错误情况作出反应的首选方法。

对错误的替代反应可能是 return 特殊错误代码值。这有一些缺点,例如:

  • 如果您始终必须牢记错误代码等,使用函数会变得更加困难。
  • 调用方可能忘记处理错误。当您忘记异常处理程序时,您将得到未处理的异常。您很有可能会检测到此错误。
  • 错误可能过于复杂,无法用单个错误代码正确表达。异常可以是包含您需要的所有信息的对象。
  • 错误在明确定义的位置(异常处理程序)处理。这将使您的代码比在每次调用后检查 return 值的特殊值更清晰。

在您的示例中,答案取决于函数的语义:

如果空向量是可接受的 return 值,那么您应该使用 return 如果不是找到一对。

如果期望总是有匹配对,那么找不到对显然是错误的。在这种情况下,您应该使用 throw