27-6 重新抛出异常
有时您可能会遇到这样的情况:想要捕获异常,但不想(或无法)在捕获到异常时对其进行完全处理。这种情况常见于您想要记录错误,但将问题传递给调用者进行实际处理时。
当函数可以使用返回码时,这很简单。请看以下示例:
Database* createDatabase(std::string filename)
{
Database* d {};
try
{
d = new Database{};
d->open(filename); // assume this throws an int exception on failure
return d;
}
catch (int exception)
{
// Database creation failed
delete d;
// Write an error to some global logfile
g_log.logError("Creation of Database failed");
}
return nullptr;
}
在上面的代码片段中,该函数负责创建一个数据库对象、打开数据库并返回该数据库对象。如果出现错误(例如传入了错误的文件名),异常处理程序会记录错误,并返回一个空指针。
现在考虑以下函数:
int getIntValueFromDatabase(Database* d, std::string table, std::string key)
{
assert(d);
try
{
return d->getIntValue(table, key); // throws int exception on failure
}
catch (int exception)
{
// Write an error to some global logfile
g_log.logError("getIntValueFromDatabase failed");
// However, we haven't actually handled this error
// So what do we do here?
}
}
如果此函数执行成功,它将返回一个整数值——任何整数值都可能是有效值。
但如果 getIntValue() 函数出错怎么办?在这种情况下,getIntValue() 会抛出一个整数异常,该异常会被 getIntValueFromDatabase() 函数中的 catch 代码块捕获,并记录错误信息。但是,我们该如何告知 getIntValueFromDatabase() 的调用者发生了错误呢?与之前的例子不同,这里没有合适的返回值可以使用(因为任何整数返回值都可能是有效的)。
抛出一个新的异常
一个显而易见的解决方法是抛出一个新的异常。
int getIntValueFromDatabase(Database* d, std::string table, std::string key)
{
assert(d);
try
{
return d->getIntValue(table, key); // throws int exception on failure
}
catch (int exception)
{
// Write an error to some global logfile
g_log.logError("getIntValueFromDatabase failed");
// Throw char exception 'q' up the stack to be handled by caller
throw 'q';
}
}
在上面的例子中,程序捕获了 getIntValue() 函数抛出的 int 异常,记录了错误,然后抛出了一个字符值为 'q' 的新异常。虽然在 catch 代码块中抛出异常看起来很奇怪,但这却是允许的。记住,只有在 try 代码块中抛出的异常才能被捕获。这意味着在 catch 代码块中抛出的异常不会被它所在的 catch 代码块捕获。相反,它会沿着调用栈向上抛出,直到被调用者捕获。
catch 块抛出的异常可以是任何类型的异常——它不必与刚刚捕获的异常类型相同。
错误地重新抛出异常
另一种方法是重新抛出同一个异常。一种方法如下:
int getIntValueFromDatabase(Database* d, std::string table, std::string key)
{
assert(d);
try
{
return d->getIntValue(table, key); // throws int exception on failure
}
catch (int exception)
{
// Write an error to some global logfile
g_log.logError("getIntValueFromDatabase failed");
throw exception;
}
}
虽然这种方法可行,但它也存在一些缺点。首先,它抛出的异常与捕获到的异常并不完全相同——而是抛出一个变量复制初始化的异常。尽管编译器可以省略这个复制操作,但它可能不会这样做,因此这种方法可能会降低性能。
但值得注意的是,请考虑以下情况:
int getIntValueFromDatabase(Database* d, std::string table, std::string key)
{
assert(d);
try
{
return d->getIntValue(table, key); // throws Derived exception on failure
}
catch (Base& exception)
{
// Write an error to some global logfile
g_log.logError("getIntValueFromDatabase failed");
throw exception; // Danger: this throws a Base object, not a Derived object
}
}
在这种情况下,getIntValue() 抛出了一个派生对象,但 catch 块捕获的是一个基类引用。这没问题,因为我们知道基类引用可以指向派生对象。然而,当我们抛出异常时,抛出的异常是从变量 exception 复制初始化的。变量 exception 的类型是 Base,因此复制初始化的异常的类型也是 Base(而不是 Derived!)。换句话说,我们的派生对象被切片了!
您可以在以下程序中看到这一点:
#include <iostream>
class Base
{
public:
Base() {}
virtual void print() { std::cout << "Base"; }
};
class Derived: public Base
{
public:
Derived() {}
void print() override { std::cout << "Derived"; }
};
int main()
{
try
{
try
{
throw Derived{};
}
catch (Base& b)
{
std::cout << "Caught Base b, which is actually a ";
b.print();
std::cout << '\n';
throw b; // the Derived object gets sliced here
}
}
catch (Base& b)
{
std::cout << "Caught Base b, which is actually a ";
b.print();
std::cout << '\n';
}
return 0;
}
打印出来的内容:

第二行表明 Base 实际上是一个 Base 对象而不是一个 Derived 对象,这证明 Derived 对象被切片了。
重新抛出异常(正确的方法)
幸运的是,C++ 提供了一种方法来重新抛出刚刚捕获到的同一个异常。为此,只需在 catch 代码块中使用 throw 关键字(无需关联变量),如下所示:
#include <iostream>
class Base
{
public:
Base() {}
virtual void print() { std::cout << "Base"; }
};
class Derived: public Base
{
public:
Derived() {}
void print() override { std::cout << "Derived"; }
};
int main()
{
try
{
try
{
throw Derived{};
}
catch (Base& b)
{
std::cout << "Caught Base b, which is actually a ";
b.print();
std::cout << '\n';
throw; // note: We're now rethrowing the object here
}
}
catch (Base& b)
{
std::cout << "Caught Base b, which is actually a ";
b.print();
std::cout << '\n';
}
return 0;
}
打印出来的内容:

这个看似没有抛出任何特定异常的 throw 关键字,实际上会重新抛出刚刚捕获到的同一个异常。不会创建任何副本,这意味着我们不必担心会因副本复制或切片而导致的性能下降。
如果需要重新抛出异常,则应优先选择此方法而非其他方法。
规则:
重新抛出同一个异常时,只需单独使用 throw 关键字即可。

浙公网安备 33010602011771号