Namespaces
Variants

Throwing exceptions

From cppreference.net
C++ language
General topics
Flow control
Conditional execution statements
Iteration statements (loops)
Jump statements
Functions
Function declaration
Lambda function expression
inline specifier
Dynamic exception specifications ( until C++17* )
noexcept specifier (C++11)
Exceptions
throw -expression
try block
Namespaces
Types
Specifiers
constexpr (C++11)
consteval (C++20)
constinit (C++20)
Storage duration specifiers
Initialization
Expressions
Alternative representations
Literals
Boolean - Integer - Floating-point
Character - String - nullptr (C++11)
User-defined (C++11)
Utilities
Attributes (C++11)
Types
typedef declaration
Type alias declaration (C++11)
Casts
Memory allocation
Classes
Class-specific function properties
Special member functions
Templates
Miscellaneous

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

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

目录

异常对象

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

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

异常对象的构造与析构

给定异常对象的类型为 T

  • obj 为类型 const T 的左值,从 obj 对类型 T 对象进行的 拷贝初始化 必须是良构的。
  • T 为类类型:

异常对象的内存分配方式未作规定。唯一的保证是存储空间绝不会通过全局 分配函数 进行分配。

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

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

该析构操作紧接在处理程序中“形参列表”声明的对象析构之后发生。

(C++11 前)

异常对象的潜在析构点包括:

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

在异常对象的所有潜在析构点中,存在一个未指明的最终析构点用于销毁异常对象。所有其他析构点均 先序于 该最终析构点发生。随后实现可以以未指明的方式释放异常对象的内存。

(C++11 起)

throw 表达式

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


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

  1. expression 执行 数组到指针 函数到指针 的标准转换。
  2. ex 为转换结果:
  • 异常对象的类型通过移除 ex 类型的所有顶层 cv 限定符来确定。
  • 异常对象通过 复制初始化 ex 进行初始化。

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

try
{
    // 抛出新异常 123
    throw 123;
}
catch (...) // 捕获所有异常
{
    // 对异常 123 进行(部分)响应
    throw; // 将异常传递给其他处理程序
}

栈展开

异常对象构造完成后,控制流会逆向(沿调用栈向上)回溯,直至抵达 try 的起始处。此时,系统会按出现顺序将关联的所有异常处理器的参数与异常对象的类型进行比对,以寻找 匹配项 。若未找到匹配项,控制流会持续展开栈直至下一个 try 块,并重复此过程。若找到匹配项,控制流会跳转至匹配的异常处理器。

当控制流沿调用栈向上传递时,会按构造函数完成顺序的逆序,调用所有具有 自动存储期 且自进入对应 try 代码块以来已构造但尚未析构的对象的析构函数。若从局部变量或 return 语句中使用的临时变量的析构函数抛出异常,则同时会调用从该函数返回的对象的析构函数。

如果从对象的构造函数或(罕见情况)析构函数中抛出异常(无论对象的存储期如何),将按照其构造函数完成顺序的逆序,为所有已完全构造的非静态非变体成员和基类调用析构函数。 类联合体 的变体成员仅在构造函数展开时会被销毁,如果在初始化和销毁期间活动成员发生改变,则行为未定义。

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

(since C++11)

若异常从由 new表达式 调用的构造函数抛出,则调用匹配的 解分配函数 (若可用)。

这个过程被称为 栈展开

如果在异常对象初始化之后、异常处理程序开始之前,任何被栈展开机制直接调用的函数因抛出异常而退出,则将调用 std::terminate 。这类函数包括作用域退出时自动存储期对象的 析构函数 ,以及用于初始化按值捕获参数的异常对象(若未发生 拷贝消除 )所调用的拷贝构造函数。

如果异常被抛出但未被捕获,包括从 std::thread 初始函数、主函数、以及任何静态或线程局部对象的构造函数或析构函数中逃逸的异常,则将调用 std::terminate 。对于未捕获异常是否进行栈回溯是由实现定义的。

注释

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

try
{
    std::string("abc").substr(10); // 抛出 std::out_of_range 异常
}
catch (const std::exception& e)
{
    std::cout << e.what() << '\n';
//  throw e; // 复制初始化一个新的 std::exception 类型异常对象
    throw;   // 重新抛出 std::out_of_range 类型的异常对象
}

throw 表达式被归类为类型为 void 纯右值表达式 。与其他表达式类似,它可以作为另一个表达式的子表达式,最常见于 条件运算符 中:

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
{
    // 创建A基类子对象
    // 创建B的a1成员
    // 创建B的a2成员失败
    // 栈展开时销毁B的a1成员
    // 栈展开时销毁A基类子对象
    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++标准。

缺陷报告 适用版本 发布时行为 正确行为
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 前)