throw 表达式

< cpp‎ | language

对错误条件发信号,并执行错误处理代码。

语法

throw 表达式 (1)
throw (2)

解释

更多关于 trycatch(异常处理)块的信息见 try-catch 块
1) 首先,从 表达式 复制初始化异常对象
  • 这可能调用右值表达式的移动构造函数
  • 这亦可能通过如 return 语句中一般的二步重载决议调用左值表达式的移动构造函数,若它们指名局部变量或是函数或 catch 子句的形参,且其作用域不超出最内层外围 try 块(若存在)
(C++17 起)
  • 即使复制初始化选择移动构造函数,从左值复制初始化仍必须为良式,且析构函数必须可访问
(C++14 起)
然后转移控制给最近进入其复合语句或成员初始化器列表,且未由此执行线程退出的,拥有匹配类型的异常处理块
2) 重抛当前处理的异常。中止当前 catch 块的执行并将控制转移到下个匹配的异常处理块(但不是到同一 try 块的下个 catch 子句:其复合语句被认为已经‘退出’),重用既存的异常对象:不生成新对象。此形式只在现在正在处理异常时允许(其他情况中使用时将调用 std::terminate)。对于构造函数,关联到函数 try 块 的 catch 子句必须通过重抛出退出。

关于在异常处理期间引发错误,见 std::terminatestd::unexpected

异常对象

异常对象是由 throw 表达式在未指明的存储中构造的临时对象。

异常对象的类型是除去顶层 cv 限定符表达式 的静态类型。数组与函数类型分别调整到指针和函数指针类型。若异常对象的类型是不完整类型或除了指向(可有 cv 限定的)void 的指针以外的不完整类型的指针,则该 throw 表达式导致编译时错误。若 表达式 的类型为类类型,则其复制/移动构造函数和析构函数必须可访问,纵使发生复制消除也是如此。

不同于其他临时对象,异常对象在初始化 catch 子句形参时被认为是左值,故它可用左值引用捕捉、修改及重抛。

异常对象持续到最后一条不以重抛而退出的 catch 子句(若不以重抛而退出,则它紧跟 catch 子句的形参销毁之后被销毁),或持续到引用此对象的最后一个 std::exception_ptr 被销毁(该情况下异常对象正好在 std::exception_ptr 的析构函数返回前被销毁)。

栈回溯

一旦构造好异常对象,控制流即反向(沿调用栈向上)直至它抵达一个 try 块的起点,在该点按出现顺序将其每个关联的 catch 块的形参和异常对象的类型进行比较,以找到一个匹配(此过程的细节见 try-catch)。若找不到匹配,则控制流继续回溯栈直至下个 try 块,此后亦然。若找到匹配,则控制流跳到匹配的 catch 块。

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

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

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

(C++11 起)

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

此过程被称为栈回溯(stack unwinding)

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

若异常被抛出但未被捕获,包括从 std::thread 的启动函数,main 函数,及任何静态或线程局部对象的构造函数或析构函数中脱离的异常,则调用 std::terminate。对未捕获异常是否进行任何栈回溯是由实现定义的。

注解

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

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

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++ 标准。

DR 应用于 出版时的行为 正确行为
CWG 1866 C++14 从构造函数栈回溯时会泄露变体成员 变体成员被销毁
CWG 1863 C++14 在抛出时对仅移动异常对象不要求复制构造函数,但允许之后复制 要求复制构造函数
CWG 2176 C++14 从局部变量的析构函数抛出时会跳过返回值的析构函数 添加函数返回值到回溯过程

参阅