有时你会遇到这样一种情况:想捕获某个异常,但并不打算(或无法)在当前位置彻底处理它。最常见的场景是记录错误日志后,再把问题交给更上层的调用者去处理。
当函数能够使用返回码时,这一点很容易做到。例如:
Database* createDatabase(std::string filename)
{
Database* d{};
try
{
d = new Database{};
d->open(filename); // 假设失败时抛出 int 异常
return d;
}
catch (int exception)
{
// 数据库创建失败
delete d;
g_log.logError("Creation of Database failed");
}
return nullptr;
}
在上面的代码中,函数负责创建并打开一个 Database
对象。一旦出错(如传入错误文件名),异常处理器先记录日志,然后返回空指针,将错误信息反馈给调用者。
现在考虑另一个函数:
int getIntValueFromDatabase(Database* d, std::string table, std::string key)
{
assert(d);
try
{
return d->getIntValue(table, key); // 失败时抛出 int 异常
}
catch (int exception)
{
g_log.logError("getIntValueFromDatabase failed");
// 然而错误尚未真正处理
// 那这里应该做什么?
}
}
若函数执行成功,它返回一个整数值——任何整数值都可能是合法结果。
一旦 getIntValue()
出错,它会抛出 int
异常,被 getIntValueFromDatabase()
捕获并记录日志。但随后如何通知调用者发生了错误?与前面的例子不同,这里无法通过返回码表达错误,因为任何 int
值都可能是合法数据。
抛出新的异常
一种显而易见的解决方案是抛出一个新的异常:
int getIntValueFromDatabase(Database* d, std::string table, std::string key)
{
assert(d);
try
{
return d->getIntValue(table, key);
}
catch (int exception)
{
g_log.logError("getIntValueFromDatabase failed");
throw 'q'; // 向调用者抛出 char 异常
}
}
上述代码捕获到 int
异常后记录日志,再抛出一个 char
类型的异常。虽然看起来奇怪,但在 catch 块内抛出异常是允许的。记住:只有 try 块内抛出的异常才会被该 try 块捕获。因此,catch 块内抛出的异常不会再次被自身捕获,而是继续向调用栈上层传播。
此外,catch 块内抛出的异常类型可以与刚捕获的异常类型不同。
重新抛出异常(错误做法)
另一种选择是重新抛出同一个异常。一种常见但不够理想的做法如下:
catch (int exception)
{
g_log.logError("getIntValueFromDatabase failed");
throw exception; // 抛出 exception 的副本
}
虽然可行,但存在两个问题:
- 性能:抛出的并非原异常对象,而是变量
exception
的副本。尽管编译器可以省略复制,但也可能不会,因此可能存在额外开销。 - 对象切片:考虑以下场景:
catch (Base& exception)
{
...
throw exception; // 危险!只会抛出 Base 对象,而非 Derived 对象
}
如果实际抛出的是 Derived
对象,而 catch 捕获的是 Base&
,则 throw exception;
只会复制 Base
部分,导致对象切片。
验证程序:
#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&, actually a ";
b.print();
std::cout << '\n';
throw b; // 发生切片
}
}
catch (Base& b)
{
std::cout << "Caught Base&, actually a ";
b.print();
std::cout << '\n';
}
return 0;
}
输出:
Caught Base&, actually a Derived
Caught Base&, actually a Base
第二行输出“Base”而非“Derived”,证明发生了对象切片。
重新抛出异常(正确做法)
C++ 提供了无操作数 throw 来重新抛出完全相同的异常对象:
catch (Base& b)
{
...
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&, actually a ";
b.print();
std::cout << '\n';
throw; // 无操作数 throw
}
}
catch (Base& b)
{
std::cout << "Caught Base&, actually a ";
b.print();
std::cout << '\n';
}
return 0;
}
输出:
Caught Base&, actually a Derived
Caught Base&, actually a Derived
无操作数 throw
会重新抛出刚刚捕获的完全相同的异常对象,不会产生任何副本,因此既避免了性能损耗,也避免了对象切片。
规则
如需重新抛出同一异常,应使用无操作数的 throw;
。