使用throw来替换C++非void函数中的return

use*_*490 47 c++ exception-handling

在C++函数中,returnthrow?替换是一个好习惯吗?例如,我有以下代码

// 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";
}
Run Code Online (Sandbox Code Playgroud)

上面的代码用我的GCC 7.2编译.

eer*_*ika 40

在C++函数中,用throw替换return是一个好习惯吗?

返回不是一般可以被投掷取代的东西.

在无法返回任何内容的特殊情况下,抛出异常可能是退出函数的有效方法.

是否是"良好实践",什么案例是"特殊"是主观的.例如,对于像你这样的搜索功能,可能没有解决方案并不奇怪,我认为投掷不合适.

投掷通常还有其他选择.将您的算法与类似的东西进行比较std::string::find,返回子字符串开头的索引.如果子串不存在,则返回"非值" std::string::npos.您可以这样做,并确定在找不到结果时返回索引-1.在没有为此目的保留任何现有表示的情况下,还有一种向表单添加非值表示的通用方法:std::optional.

PS向量可能不是返回一对数字的好选择.std::pair可能会更好,或者如果你有数字的好名字,可以是自定义类.

  • @Joker_vD:**这是可能的,但在"正常"实现上表现得像废话**.除非他们可以优化抛出/捕获,否则启动堆栈展开异常调度代码(必须读取`.eh_frame`元数据)比`ret`指令要昂贵得多. (7认同)
  • 好吧,我曾经看过一个小代码库,其中void函数通过抛出它们返回结果,并且`try-catch`被用作一种模式匹配工具.我不得不承认,这是一个奇怪的设计,但它显示了如何用`throw`*替换`return`一般*. (5认同)
  • @Joker_vD 坦率地说,我从未见过所有返回路径都被抛出的函数(“未实现”样式异常除外)。但我承认这在技术上是合法的 C++。所以,当我说退货*不能*被替换时,我在技术上撒了一点谎。 (2认同)
  • 当然,返回一个错误 _value_ 像 `std::string::npos` 意味着调用者可能完全忘记检查它并将它传递给其他一些代码,最终产生难以追溯到原始原因的废话。这个问题是异常(或可选类型)的全部原因。 (2认同)

Fra*_*Boi 30

这个答案的概念来自Bjarne Stroustrup的C++编程语言.

简短的回答

是的,异常抛出可以用作返回值方法.以下是二叉树搜索功能的示例:

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;
}
Run Code Online (Sandbox Code Playgroud)

但是,应该避免,因为:

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

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

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

更长的答案

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

首先,例外是​​管理同步事件,而不是异步事件.这是第一个限制.

有人可能会将异常处理机制视为另一种控制结构,这是将值返回给调用者的另一种方法.

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

什么时候可能坚持"异常处理是一个错误处理"视图.完成此操作后,代码分为两类:普通代码和错误处理代码.这使代码更易于理解.此外,异常机制的实现基于这个简单模型基于异常的使用的假设而被优化.

所以基本上应该避免使用异常来返回值,因为

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

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

  • 嵌入式系统的时间关键组件,必须保证在指定的最大时间内完成操作.在不存在的,可以准确地估计的最大时间的例外从传播工具throw,以catch替代的错误处理方法必须使用.
  • 一个大的旧程序,其中资源管理是临时混乱(使用裸指针进行非系统管理的免费存储,news并且delete)而不是依赖于某些系统方案,例如资源句柄(strings vectors).

在上述情况下,传统的异常前方法是首选.


Cod*_*ice 19

return并且throw有两个不同的目的,不应该被认为是可以互换的.使用return时,你有一个有效的结果发回给调用者.另一方面,throw在发生某些异常行为时使用.您可以throw通过使用标准库中的函数并注意它们何时抛出异常来了解其他程序员的使用方式.


Ped*_* LM 15

除了其他答案之外,还有性能:与子句相比,捕获异常会产生运行时开销(请参阅此答案)if.

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

  • 函数调用/ ret在3到4 GHz的CPU上需要大约1纳秒,并且可以与周围的工作重叠.大约1000倍的声音听起来似乎有道理.但请记住,在现代CPU上,微秒是*数千*的时钟周期(在此期间,它们可能每个时钟执行1到3条指令,甚至在高ILP代码中执行更多),甚至比高速缓存未命中时间长10倍这一直是RAM和停止无序执行.即使在内联之后,try/catch而不是return也可能不会优化掉,所以你最终会得到比你```/`ret`指令更多的东西. (7认同)

Bri*_*ian 10

函数在无法满足其后置条件时应抛出异常.(有些函数也可能在不满足前提条件时抛出异常;这是我不会在这里讨论的另一个主题.)因此,如果你的函数必须从向量求和到给定目标返回一对整数,那么它别无选择,只能在无法找到异常时抛出异常.但是如果函数的契约允许它返回一个值来表明它无法找到这样的一对,那么它就不应该抛出异常,因为它有能力履行合同.

在你的情况下:

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

通常,C++程序员喜欢编写不需要抛出异常的函数,以报告容易预期和本地处理的"失望",例如搜索失败.在其他地方广泛讨论了返回值而不是抛出异常的优点,因此我不会在此重复讨论.

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

  • 该函数是一个构造函数,它根本无法建立其类的不变量.它必须抛出异常以确保调用代码不会看到损坏的对象.
  • 出现"特殊"情况,应该在比呼叫者更高的级别处理.呼叫者可能不知道如何从病情中恢复.在这种情况下,使用异常机制可以使调用者不必在异常情况发生时弄清楚如何继续.


arm*_*tus 8

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

从本质上讲,最好throw在方法中检测到重大错误时使用,但如果未检测到此类错误,则应始终保持清洁返回.你能做出替代吗?也许,也许它会在逻辑上起作用......但这不是一个好的做法,当然也不是一个流行的实现.


Abi*_*n47 7

不,throw不是一个很好的语义替代品return.您只希望throw在代码执行了不应该执行的操作时使用,而不是表示函数的完全有效的否定结果.

作为一般规则,异常旨在表示在执行代码期间发生异常或意外事件的时间.看一下函数的用途,在传递的向量求和中没有两个整数的出现是函数的target一个非常可能的结果,因此结果不是异常的,因此不应该被视为异常.


dis*_*ame 5

您的问题与语言无关。

Return和Throw具有不同的目的。

  • Return是用于返回值;而,
  • 抛出是引发异常。和,
    • 在真正例外的情况下应该抛出异常。

奖励内容(摘自史蒂夫·麦康奈尔的《 Code Complete 2》)

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

  1. 从应该返回某物计数的方法中返回一个类似-1 的中性值
  2. 反向行驶时,在汽车的数字里程表上返回最接近的合法值(例如0);
  3. 当您每秒读取一次时,返回与上次调用相同的值,例如温度计上的温度读数,并且您知道在这么短的时间间隔内读数不会显着不同。
  4. 返回下一个有效数据段,例如当您从表中读取行并且特定行已损坏时;
  5. 设置/返回在C ++库调用中很常见的错误状态/代码/对象
  6. 在警报框中显示一条消息,但请注意不要过多散布可帮助恶意行为者的信息;
  7. 记录到文件 ;
  8. 抛出一个异常,就像您在思考一样;
  9. 如果这是系统的设计方式,请调用中央错误处理程序/例程
  10. 关机

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

您应该采用哪种方法来解决“ 正确性与鲁棒性”问题。您是否希望程序绝对正确并在遇到错误情况时关闭,还是希望程序健壮并在某些时候无法遵循所需的路径继续执行?


Rob*_*juk 5

仅仅因为该函数将作为最后一个调用而抛出,并不意味着它正在代替return。这只是逻辑流程。

问题不应该是:

用throw代替return是一个好习惯吗?

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

如果要向vector永远不为空的函数的用户保证,则抛出异常是一个好主意。

如果要保证函数不会抛出异常,而是vector在某些条件下返回空值,那么抛出则是个坏主意。

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