throw 表达式

来自cppreference.com
< cpp‎ | language

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

语法

throw 表达式 (1)
throw (2)

解释

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

关于在异常处理期间引发错误,见 std::terminatestd::unexpected (C++17 前)

异常对象

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

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

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

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

栈回溯

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

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

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

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

(C++11 起)

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

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

如果由栈回溯机制所直接调用的函数在异常对象初始化后且在异常处理块开始执行前以异常退出,那么就会调用 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 从局部变量的析构函数抛出时会跳过返回值的析构函数 添加函数返回值到回溯过程

参阅