如何抛出C++异常

390

我对异常处理的理解非常匮乏(例如,如何自定义throw、try、catch语句以实现自己的目的)。

例如,我定义了以下函数:int compare(int a, int b){...}

当a或b为负数时,我希望该函数抛出一个带有一些信息的异常。

在函数定义中,我应该如何处理这个问题?


9
避免不必要的异常。如果您不希望调用者传递负值,请通过在函数签名中指定“unsigned int”作为参数来更加明显地表明。然而,我认为只有在实际出现异常情况时才应该抛出和捕获异常。 - AJG85
2
@Mark:我最初误解了这个问题,认为它是关于是否应该在函数上使用throw()异常规范。 - Oliver Charlesworth
更通用的是,这里有一个链接(https://www.w3schools.com/cpp/cpp_exceptions.asp),它展示了你可以使用`throw 5;或者throw "Bad!";以及catch (...) { }`等等... - Andrew
5个回答

516

简单:

#include <stdexcept>

int compare( int a, int b ) {
    if ( a < 0 || b < 0 ) {
        throw std::invalid_argument( "received negative value" );
    }
}

标准库提供了一组不错的内置异常对象,你可以抛出它们。请记住,你应该始终按值抛出并按引用捕获异常:

try {
    compare( -1, 3 );
}
catch( const std::invalid_argument& e ) {
    // do stuff with exception... 
}
你可以在每个try后面添加多个catch()语句,这样你就可以单独处理不同类型的异常。 你也可以重新抛出异常:
catch( const std::invalid_argument& e ) {
    // do something

    // let someone higher up the call stack handle it if they want
    throw;
}

无论类型如何,都可以捕获异常:

catch( ... ) { };

39
你应该始终将异常捕获为常量。 - Adrian Cornish
2
@TerryLiYifeng 如果自定义异常更有意义,那就使用它。你仍然可能想从std::exception派生并保持接口相同。 - nsanders
2
再次点赞,但我认为const非常重要——因为它强调了它现在是一个临时对象,所以修改是无用的。 - Adrian Cornish
3
@AdrianCornish:但它并不完全是临时的。非常量的捕获可能很有用 - GManNickG
33
通常情况下,你会使用简单的throw;语句重新抛出原始对象并保持其类型,而不是throw e;语句(抛出捕获对象的副本,可能会改变其类型)。 - Mike Seymour
显示剩余7条评论

26

尽管这个问题比较旧,而且已经有了答案,但我想补充一下如何在C++11中进行适当的异常处理:

使用std::nested_exceptionstd::throw_with_nested

在StackOverflow的这里这里有描述如何在代码内部获取异常的回溯信息,无需调试器或繁琐的日志记录,只需编写一个适当的异常处理程序即可重新抛出嵌套异常。

由于您可以在任何派生的异常类上进行此操作,因此您可以添加许多信息到此类回溯中!您还可以查看我的GitHub上的MWE,其中回溯将类似于:

Library API: Exception caught in function 'api_function'
Backtrace:
~/Git/mwe-cpp-exception/src/detail/Library.cpp:17 : library_function failed
~/Git/mwe-cpp-exception/src/detail/Library.cpp:13 : could not open file "nonexistent.txt"

21

只需在必要的地方添加throw,并在调用者中添加处理错误的try块。按照惯例,您应该仅抛出从std::exception派生的对象,因此首先需要包含<stdexcept>

int compare(int a, int b) {
    if (a < 0 || b < 0) {
        throw std::invalid_argument("a or b negative");
    }
}

void foo() {
    try {
        compare(-1, 0);
    } catch (const std::invalid_argument& e) {
        // ...
    }
}

此外,还要研究 Boost.Exception


12

补充此答案,因为此时似乎没有必要为此Q&A创建另一个答案。

在您创建自己的自定义异常(派生自std::exception)的情况下,当捕获“所有可能”的异常类型时,应始终以可能被捕获的“最派生”异常类型开始catch子句。请参见示例(不要做以下事情的示例):

#include <iostream>
#include <string>

using namespace std;

class MyException : public exception
{
public:
    MyException(const string& msg) : m_msg(msg)
    {
        cout << "MyException::MyException - set m_msg to:" << m_msg << endl;
    }

   ~MyException()
   {
        cout << "MyException::~MyException" << endl;
   }

   virtual const char* what() const throw () 
   {
        cout << "MyException::what" << endl;
        return m_msg.c_str();
   }

   const string m_msg;
};

void throwDerivedException()
{
    cout << "throwDerivedException - thrown a derived exception" << endl;
    string execptionMessage("MyException thrown");
    throw (MyException(execptionMessage));
}

void illustrateDerivedExceptionCatch()
{
    cout << "illustrateDerivedExceptionsCatch - start" << endl;
    try 
    {
        throwDerivedException();
    }
    catch (const exception& e)
    {
        cout << "illustrateDerivedExceptionsCatch - caught an std::exception, e.what:" << e.what() << endl;
        // some additional code due to the fact that std::exception was thrown...
    }
    catch(const MyException& e)
    {
        cout << "illustrateDerivedExceptionsCatch - caught an MyException, e.what::" << e.what() << endl;
        // some additional code due to the fact that MyException was thrown...
    }

    cout << "illustrateDerivedExceptionsCatch - end" << endl;
}

int main(int argc, char** argv)
{
    cout << "main - start" << endl;
    illustrateDerivedExceptionCatch();
    cout << "main - end" << endl;
    return 0;
}

注意:

  1. 正确的顺序应该是相反的,即先使用 catch (const MyException& e),然后是 catch (const std::exception& e)

  2. 正如您所看到的,当您按原样运行程序时,将执行第一个 catch 子句(这可能不是您想要的结果)。

  3. 尽管第一个 catch 子句捕获的类型是 std::exception 类型,但将调用“适当”的 what() 版本 - 因为它是通过引用捕获的(至少更改捕获参数 std::exception 的类型为按值 - 您将体验到“对象切片”现象)。

  4. 如果“由于 XXX 异常抛出而导致某些代码...”对于异常类型很重要,则此处存在代码错误。

  5. 如果捕获的对象是“普通”的对象,例如:class Base {};class Derived: public Base {}...

  6. 在 Ubuntu 18.04.1 上使用 g++ 7.3.0 会产生警告,指出上述问题:

在函数 'void illustrateDerivedExceptionCatch()' 中: item12Linux.cpp:48:2: 警告:将捕获类型为'MyException'的异常 catch(const MyException& e) ^~~~~

item12Linux.cpp:43:2: 警告:通过早期处理程序处理 'std::exception' catch (const exception& e) ^~~~~

再次强调,我只是想补充其他人在这里描述的答案(我认为这一点值得提及,但无法在评论中表述)。


实际上,你应该先捕获特定的异常,然后再捕获更一般的异常。在你的情况下,首先捕获catch(const MyException& e),然后再捕获catch(const exception& e)。由于exception是基类,你永远不会进入第二个作用域。 - Chief
1
@Chief - 你是100%正确的。这就是为什么我写了“你不应该做什么”的原因 :) - Guy Avraham
哦,抱歉,我可能没有注意到那个。 - Chief
有人能解释一下 virtual const char* what() const throw () {...} 这个声明是什么意思吗?这是一个名为 what 的方法还是一个名为 throw 的方法?为什么函数定义中有两对括号? - MTV

12
您可以定义在某个错误发生时抛出的消息:
throw std::invalid_argument( "received negative value" );

或者你可以像这样定义它:

std::runtime_error greatScott("Great Scott!");          
double getEnergySync(int year) {                        
    if (year == 1955 || year == 1885) throw greatScott; 
    return 1.21e9;                                      
}                                                       

通常,您会像这样使用try ... catch块:

try {
// do something that causes an exception
}catch (std::exception& e){ std::cerr << "exception: " << e.what() << std::endl; }

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