抛出异常

来自cppreference.com
< cpp‎ | language


 
 
C++ 语言
 
 
异常
try
抛出异常
处理异常
异常说明
    noexcept 说明 (C++11)
    动态说明 (C++17 前*)
noexcept 运算符 (C++11)
 

抛出异常会转移控制到处理块

异常可以从 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) 重新抛出当前正在处理的异常。
表达式 - 用来构造异常对象的表达式


抛出新的异常时,按以下方式确定它的异常对象:

  1. 表达式 进行数组到指针函数到指针标准转换。
  2. ex 为上述转换的结果:
  • 异常对象的类型是从 ex 的类型移除顶层 cv 限定得到的类型。
  • 异常对象会从 ex 复制初始化

如果程序试图在没有正在处理异常的情况下重新抛出异常,那么就会调用 std::terminate。否则会以当前异常对象重新激活异常(不会创建新的异常对象),并且该异常不再视为已捕获。

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

栈回溯

异常对象构造完成时,控制流立即反向(沿调用栈向上)直到它抵达一个 try的起点,在该点按出现顺序将它每个关联的处理块的形参和异常对象的类型进行比较,以找到一个匹配。如果找不到匹配,那么控制流继续回溯栈直到下个 try 块,以此类推。如果找到匹配,那么控制流跳到匹配的处理块。

因为控制流沿调用栈向上移动,所以它会为自进入相应 try 块之后的所有具有自动存储期的已构造但尚未销毁的对象,以它们的构造函数完成的逆序调用析构函数。当从 return 语句所使用的局部变量或临时量的构造函数中抛出异常时,从函数返回的对象的析构函数也会被调用。

如果异常从某个对象的构造函数或(罕见地)从析构函数抛出(不管该对象的存储期),那么就会对所有已经完整构造的非静态非变体成员和基类以构造函数完成的逆序调用析构函数。联合体式的类的变体成员只会在从构造函数中回溯的情况中销毁,且如果初始化与销毁之间改变了活动成员,那么行为未定义。

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

(C++11 起)

如果从 new 表达式所调用的构造函数抛出异常,那么调用匹配的解分配函数,如果它可用。

此过程被称为栈回溯

如果由栈回溯机制所直接调用的函数在异常对象初始化后且在异常处理块开始执行前以异常退出,那么就会调用 std::terminate。这种函数包括退出作用域的具有自动存储期的对象的析构函数,和为初始化以值捕获的实参而调用(如果没有被消除)的异常对象的复制构造函数。

如果异常被抛出但未被捕获,包括从 std::thread 的启动函数,main 函数,及任何静态或线程局部对象的构造函数或析构函数中脱离的异常,那么就会调用 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 << ") 已成功构造\n"; }
    ~A() { std::cout << "A(" << n << ") 已销毁\n"; }
};
 
int foo()
{
    throw std::runtime_error("错误");
}
 
struct B
{
    A a1, a2, a3;
 
    B() try : a1(1), a2(foo()), a3(3)
    {
        std::cout << "B 已成功构造\n";
    }
    catch(...)
    {
    	std::cout << "B::B() 因异常退出\n";
    }
 
    ~B() { std::cout << "B 已摧毁\n"; }
};
 
struct C : A, B
{
    C() try
    {
        std::cout << "C::C() 已成功完成\n";
    }
    catch(...)
    {
        std::cout << "C::C() 因异常退出\n";
    }
 
    ~C() { std::cout << "C 已销毁\n"; }
};
 
int main () try
{
    // 创建 A 基类子对象
    // 创建 B 的成员 a1
    // 创建 B 的成员 a2 失败
    // 回溯销毁 B 的 a1 成员
    // 回溯销毁 A 基类子对象
    C c;
}
catch (const std::exception& e)
{
    std::cout << "main() 创建 C 失败,原因:" << e.what();
}

输出:

A(0) 已成功构造
A(1) 已成功构造
A(1) 已销毁
B::B() 因异常退出
A(0) 已销毁
C::C() 因异常退出
main() 创建 C 失败,原因:错误

缺陷报告

下列更改行为的缺陷报告追溯地应用于以前出版的 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 Throwing an exception [expr.throw]
  • 14.2 Throwing an exception [except.throw]
  • C++20 标准(ISO/IEC 14882:2020):
  • 7.6.18 Throwing an exception [expr.throw]
  • 14.2 Throwing an exception [except.throw]
  • C++17 标准(ISO/IEC 14882:2017):
  • 8.17 Throwing an exception [expr.throw]
  • 18.1 Throwing an exception [except.throw]
  • C++14 标准(ISO/IEC 14882:2014):
  • 15.1 Throwing an exception [except.throw]
  • C++11 标准(ISO/IEC 14882:2011):
  • 15.1 Throwing an exception [except.throw]
  • C++03 标准(ISO/IEC 14882:2003):
  • 15.1 Throwing an exception [except.throw]
  • C++98 标准(ISO/IEC 14882:1998):
  • 15.1 Throwing an exception [except.throw]

参阅