命名空间
变体
操作

抛出异常

来自 cppreference.cn
< cpp‎ | language
 
 
C++ 语言
通用主题
流程控制
条件执行语句
if
迭代语句(循环)
for
range-for (C++11)
跳转语句
函数
函数声明
Lambda 函数表达式
inline 说明符
动态异常规范 (until C++17*)
noexcept 说明符 (C++11)
异常
throw 表达式
try
命名空间
类型
说明符
const/volatile
decltype (C++11)
auto (C++11)
constexpr (C++11)
consteval (C++20)
constinit (C++20)
存储期说明符
初始化
 
 
异常
try
抛出异常
异常处理
异常规范
    noexcept 规范 (C++11)
    动态规范 (until C++17*)
noexcept 运算符 (C++11)
 

抛出异常会将控制权转移给处理程序

异常可以从throw 表达式抛出,以下上下文也可能抛出异常

目录

[编辑] 异常对象

抛出异常会初始化一个具有动态存储期的对象,称为异常对象

如果异常对象的类型是以下类型之一,则程序是非良构的

[编辑] 构造和析构异常对象

给定异常对象的类型为 T

  • objconst T 类型的左值,则从 obj 复制初始化类型为 T 的对象必须是良构的。
  • 如果 T 是类类型

异常对象的内存以未指定的方式分配。唯一的保证是存储永远不会由全局分配函数分配。

如果处理程序通过重新抛出退出,则控制权会传递给同一异常对象的另一个处理程序。在这种情况下,异常对象不会被析构。

当异常的最后一个剩余活动处理程序以重新抛出以外的任何方式退出时,异常对象将被销毁,并且实现可以以未指定的方式释放临时对象的内存。

析构发生在处理程序中“参数列表”中声明的对象析构之后立即发生。

(直到 C++11)

异常对象的潜在析构点是

  • 当异常的活动处理程序以重新抛出以外的任何方式退出时,紧随处理程序中“参数列表”中声明的对象(如果有)的析构之后。
  • 当引用异常对象的 std::exception_ptr 类型的对象被销毁时,在 std::exception_ptr 的析构函数返回之前。

在异常对象的所有潜在析构点中,存在一个未指定的最后一个析构点。所有其他点都发生在前最后一个点之前。然后,实现可以以未指定的方式释放异常对象的内存。

(自 C++11 起)

[编辑] throw 表达式

throw 表达式 (1)
throw (2)
1) 抛出一个新的异常。
2) 重新抛出当前正在处理的异常。
表达式 - 用于构造异常对象的表达式


当抛出一个新的异常时,其异常对象按如下方式确定

  1. 表达式执行数组到指针函数到指针的标准转换。
  2. ex 为转换结果
  • 异常对象的类型通过从 ex 的类型中删除任何顶层 cv 限定符来确定。
  • 异常对象从 ex 复制初始化

如果程序尝试在当前未处理任何异常时重新抛出异常,则将调用 std::terminate。 否则,将使用现有的异常对象重新激活异常(不创建新的异常对象),并且不再认为该异常已被捕获。

try
{
    // throwing a new exception 123
    throw 123;
}
catch (...) // catch all exceptions
{
    // respond (partially) to exception 123
    throw; // pass the exception to some other handler
}

[编辑] 栈展开

一旦构造了异常对象,控制流就会向后(向上调用堆栈)工作,直到到达 try的开头,此时,将所有关联处理程序的参数按照出现顺序与异常对象的类型进行比较,以找到匹配项。 如果没有找到匹配项,则控制流将继续展开堆栈,直到下一个 try 块,依此类推。 如果找到匹配项,则控制流将跳转到匹配的处理程序。

当控制流向上移动调用堆栈时,将为自进入相应的 try 块以来构造但尚未销毁的所有具有自动存储期的对象调用析构函数,顺序与构造函数完成顺序相反。 如果从局部变量的析构函数或 return 语句中使用的临时对象的析构函数中抛出异常,则还会调用从函数返回的对象的析构函数。

如果从对象的构造函数或(极少)从对象的析构函数(无论对象的存储期如何)中抛出异常,则会为所有完全构造的非静态非变体成员和基类调用析构函数,顺序与构造函数完成顺序相反。 仅在从构造函数展开的情况下,才会销毁类 union 的变体成员;并且如果在初始化和销毁之间活动成员发生更改,则行为未定义。

如果委托构造函数在非委托构造函数成功完成后以异常退出,则会调用此对象的析构函数。

(自 C++11 起)

如果异常是从 new-expression 调用的构造函数抛出的,则会调用匹配的 释放函数(如果可用)。

此过程称为栈展开

如果在异常对象初始化之后和异常处理程序开始之前,栈展开机制直接调用的任何函数以异常退出,则会调用 std::terminate。 这些函数包括作用域已退出的具有自动存储期的对象的析构函数,以及调用(如果未省略)以初始化按值捕获参数的异常对象的复制构造函数。

如果抛出异常但未被捕获,包括从 std::thread 的初始函数、main 函数以及任何静态或线程局部对象的构造函数或析构函数中逸出的异常,则会调用 std::terminate。 对于未捕获的异常是否发生任何堆栈展开是实现定义的。

[编辑] 注解

当重新抛出异常时,必须使用第二种形式,以避免在异常对象使用继承的(典型)情况下发生对象切片

try
{
    std::string("abc").substr(10); // throws std::out_of_range
}
catch (const std::exception& e)
{
    std::cout << e.what() << '\n';
//  throw e; // copy-initializes a new exception object of type std::exception
    throw;   // rethrows the exception object of type std::out_of_range
}

throw 表达式被归类为 void 类型的prvalue 表达式。 像任何其他表达式一样,它可能是另一个表达式中的子表达式,最常见的是在条件运算符

double f(double d)
{
    return d > 1e7 ? throw std::overflow_error("too big") : d;
}
 
int main()
{
    try
    {
        std::cout << f(1e10) << '\n';
    }
    catch (const std::overflow_error& e)
    {
        std::cout << e.what() << '\n';
    }
}

[编辑] 关键字

throw

[编辑] 示例

#include <iostream>
#include <stdexcept>
 
struct A
{
    int n;
 
    A(int n = 0): n(n) { std::cout << "A(" << n << ") constructed successfully\n"; }
    ~A() { std::cout << "A(" << n << ") destroyed\n"; }
};
 
int foo()
{
    throw std::runtime_error("error");
}
 
struct B
{
    A a1, a2, a3;
 
    B() try : a1(1), a2(foo()), a3(3)
    {
        std::cout << "B constructed successfully\n";
    }
    catch(...)
    {
        std::cout << "B::B() exiting with exception\n";
    }
 
    ~B() { std::cout << "B destroyed\n"; }
};
 
struct C : A, B
{
    C() try
    {
        std::cout << "C::C() completed successfully\n";
    }
    catch(...)
    {
        std::cout << "C::C() exiting with exception\n";
    }
 
    ~C() { std::cout << "C destroyed\n"; }
};
 
int main () try
{
    // creates the A base subobject
    // creates the a1 member of B
    // fails to create the a2 member of B
    // unwinding destroys the a1 member of B
    // unwinding destroys the A base subobject
    C c;
}
catch (const std::exception& e)
{
    std::cout << "main() failed to create C with: " << e.what();
}

输出

A(0) constructed successfully
A(1) constructed successfully
A(1) destroyed
B::B() exiting with exception
A(0) destroyed
C::C() exiting with exception
main() failed to create C with: error

[编辑] 缺陷报告

以下行为变更缺陷报告被追溯应用于先前发布的 C++ 标准。

DR 应用于 已发布行为 正确行为
CWG 499 C++98 由于以下原因,无法抛出具有未知边界的数组
其类型不完整,但是可以从异常对象创建
从衰减的指针创建,没有任何问题
应用类型完成
对...的要求
而是异常对象
CWG 668 C++98 如果抛出异常,则不会调用 std::terminate
从局部非自动对象的析构函数中
调用 std::terminate
在这种情况下
CWG 1863 C++11 对于仅移动异常对象,不需要复制构造函数
抛出异常对象时,但稍后允许复制
需要复制构造函数
CWG 1866 C++98 变体成员在从构造函数堆栈展开时泄漏 变体成员已销毁
CWG 2176 C++98 从局部变量析构函数抛出
可以跳过返回值析构函数
函数返回值
添加到展开
CWG 2699 C++98 throw "EX" 实际上会抛出 char* 而不是 const char* 已更正
CWG 2711 C++98 复制初始化源
异常对象未指定
复制初始化
来自 表达式
CWG 2775 C++98 异常对象复制初始化要求不明确 已明确
CWG 2854 C++98 异常对象的存储期不明确 已明确
P1825R0 C++11 在 throw 中禁止从参数隐式移动 已允许

[编辑] 参考

  • C++23 标准 (ISO/IEC 14882:2024)
  • 7.6.18 抛出异常 [expr.throw]
  • 14.2 抛出异常 [except.throw]
  • C++20 标准 (ISO/IEC 14882:2020)
  • 7.6.18 抛出异常 [expr.throw]
  • 14.2 抛出异常 [except.throw]
  • C++17 标准 (ISO/IEC 14882:2017)
  • 8.17 抛出异常 [expr.throw]
  • 18.1 抛出异常 [except.throw]
  • C++14 标准 (ISO/IEC 14882:2014)
  • 15.1 抛出异常 [except.throw]
  • C++11 标准 (ISO/IEC 14882:2011)
  • 15.1 抛出异常 [except.throw]
  • C++03 标准 (ISO/IEC 14882:2003)
  • 15.1 抛出异常 [except.throw]
  • C++98 标准 (ISO/IEC 14882:1998)
  • 15.1 抛出异常 [except.throw]

[编辑] 参见

(直到 C++17)