自定义删除器不应抛出异常,因析构过程抛异常会触发std::terminate导致程序崩溃;正确做法是将删除器声明为noexcept,并在内部用try-catch捕获并处理所有异常,确保资源释放操作安全可靠。

当我们在C++中使用智能指针,比如
std::unique_ptr
std::shared_ptr
这听起来可能有点绝对,但它背后有非常扎实的理由。C++标准对析构函数(以及智能指针删除器这种在对象生命周期结束时被调用的机制)有着明确的期待:它们应该是
noexcept
noexcept
std::terminate
设想一下,你的智能指针在析构时,尝试释放一个资源,而这个释放操作因为某些外部因素(比如文件句柄已失效、网络连接已断开,或者底层API本身就设计得会抛异常)抛出了一个异常。如果这个异常没有在删除器内部被捕获,它就会从一个析构函数的调用链中逃逸出来。此时,C++运行时环境会陷入一个非常尴尬的境地:在一个对象正在被销毁的过程中又出现了新的异常,这与异常安全的基本原则是冲突的。它无法保证程序的稳定状态,所以最直接、最保守的反应就是终止程序,以避免更深层次的问题。
因此,我个人在实践中,几乎都会将自定义删除器视为一个“无条件成功”的操作。如果删除器确实需要执行一些复杂且可能失败的逻辑,那么这些逻辑必须被包装在一个
try-catch
catch
立即学习“C++免费学习笔记(深入)”;
#include <iostream>
#include <memory>
#include <stdexcept>
#include <cstdio> // For FILE* operations
// 模拟一个可能抛出异常的资源释放函数
// 比如关闭一个文件,但文件句柄可能无效或操作失败
void close_file_resource(FILE* fp) {
if (fp == nullptr) {
// 实际场景中,nullptr通常不会导致抛异常,但为了演示可以模拟
throw std::runtime_error("Attempted to close a null file pointer.");
}
std::cout << "Attempting to close file pointer: " << fp << std::endl;
if (fclose(fp) != 0) {
// fclose 失败会返回 EOF,但通常不会抛C++异常。
// 这里为了演示“可能抛出异常的操作”而模拟抛出。
throw std::runtime_error("Failed to close file resource properly.");
}
std::cout << "File pointer " << fp << " closed successfully." << std::endl;
}
// 错误的自定义删除器示例:会直接抛出异常
struct BadFileDeleter {
void operator()(FILE* fp) const {
std::cout << "BadFileDeleter: Executing..." << std::endl;
close_file_resource(fp); // 这里可能会抛出异常
}
};
// 正确的自定义删除器示例:内部处理异常,并声明noexcept
struct SafeFileDeleter {
void operator()(FILE* fp) const noexcept { // 明确声明noexcept
std::cout << "SafeFileDeleter: Executing..." << std::endl;
try {
close_file_resource(fp);
} catch (const std::exception& e) {
// 捕获并处理异常,例如记录日志
std::cerr << "ERROR: SafeFileDeleter caught exception for file " << fp
<< ": " << e.what() << std::endl;
// 关键:不要重新抛出异常
} catch (...) {
std::cerr << "ERROR: SafeFileDeleter caught unknown exception for file " << fp << std::endl;
}
}
};
// int main() {
// std::cout << "--- Testing BadFileDeleter (may terminate) ---" << std::endl;
// try {
// // 模拟一个文件指针
// FILE* f1 = fopen("test1.txt", "w");
// if (f1) {
// std::unique_ptr<FILE, BadFileDeleter> bad_file_ptr(f1, BadFileDeleter{});
// // bad_file_ptr 在离开作用域时析构,如果 close_file_resource 抛异常,程序会 terminate
// // 我们可以通过将 f1 设为 nullptr 来模拟抛异常
// // bad_file_ptr.release(); // 避免实际关闭,让它在析构时遇到 nullptr
// }
// // 为了演示异常,我们故意创建一个会抛异常的场景
// std::unique_ptr<FILE, BadFileDeleter> bad_file_ptr_null(nullptr, BadFileDeleter{});
// } catch (const std::exception& e) {
// std::cerr << "Caught exception in main for BadFileDeleter: " << e.what() << std::endl;
// }
// // 注意:如果 BadFileDeleter 真的抛出异常,程序可能在这里之前就已经终止了
// std::cout << "--- BadFileDeleter test finished (or terminated) ---" << std::endl;
//
// std::cout << "\n--- Testing SafeFileDeleter (should not terminate) ---" << std::endl;
// try {
// FILE* f2 = fopen("test2.txt", "w");
// if (f2) {
// std::unique_ptr<FILE, SafeFileDeleter> safe_file_ptr(f2, SafeFileDeleter{});
// // safe_file_ptr 析构时,即使 close_file_resource 抛异常,也会被内部处理,程序继续运行
// }
// // 故意创建一个会抛异常的场景
// std::unique_ptr<FILE, SafeFileDeleter> safe_file_ptr_null(nullptr, SafeFileDeleter{});
// } catch (const std::exception& e) {
// std::cerr << "Caught exception in main for SafeFileDeleter: " << e.what() << std::endl;
// }
// std::cout << "--- SafeFileDeleter test finished ---" << std::endl;
// return 0;
// }这个问题是理解智能指针异常安全性的基石。智能指针的自定义删除器,其核心职责是在所管理的对象生命周期结束时,执行必要的资源清理工作。从语义上讲,这与C++对象的析构函数行为非常相似。而C++标准对析构函数有一个非常重要的约定:它们通常应该是
noexcept
std::terminate
具体来说,当一个异常正在传播(即已经有一个异常被抛出,但尚未被捕获)时,如果此时又有另一个析构函数被调用,并且这个析构函数也抛出了异常,C++标准库会立即调用
std::terminate
noexcept
noexcept
noexcept
std::terminate
所以,让删除器抛出异常,不仅违反了C++关于析构函数异常安全的基本原则,还会让你的程序变得极其脆弱和不可预测。我个人在开发中,几乎都会把删除器设计成
noexcept
现实世界并非总是理想的,有时你确实需要在一个删除器中调用一个第三方库函数,或者一个你无法控制的API,而这些函数就是被设计成可能抛出异常的。在这种“必须”执行可能抛出异常操作的情况下,我们的策略就变成了“内部消化”异常。
最直接的方法就是使用
try-catch
catch
catch
// 示例:一个处理异常的删除器
struct NetworkConnectionDeleter {
void operator()(void* connection_handle) const noexcept {
try {
// 假设这个函数可能因为网络问题而抛出异常
disconnect_network_resource(connection_handle);
} catch (const std::exception& e) {
// 记录错误日志,但不要重新抛出
std::cerr << "Error disconnecting network resource " << connection_handle
<< ": " << e.what() << std::endl;
} catch (...) {
// 捕获所有其他未知异常
std::cerr << "Unknown error disconnecting network resource "
<< connection_handle << std::endl;
}
}
// 假设的外部函数,可能抛出异常
void disconnect_network_resource(void* handle) const {
if (handle == nullptr) {
throw std::runtime_error("Invalid network handle.");
}
// 模拟网络断开操作,可能失败
bool success = (reinterpret_cast<long>(handle) % 2 == 0); // 随机失败
if (!success) {
throw std::runtime_error("Failed to disconnect due to network issue.");
}
std::cout << "Network connection " << handle << " disconnected." << std::endl;
}
};
// 使用示例
// int main() {
// void* conn_handle1 = (void*)0x100; // 模拟一个有效的句柄
// std::unique_ptr<void, NetworkConnectionDeleter> conn_ptr1(conn_handle1, NetworkConnectionDeleter{});
// // conn_ptr1 析构时,如果 disconnect_network_resource 抛异常,会被内部处理
//
// void* conn_handle2 = (void*)0x101; // 模拟另一个句柄,可能导致失败
// std::unique_ptr<void, NetworkConnectionDeleter> conn_ptr2(conn_handle2, NetworkConnectionDeleter{});
// return 0;
// }除了内部
try-catch
close_file_safely()
release()
设计一个健壮的自定义删除器,其核心思想是“防御性编程”和“最小化职责”。
明确声明 noexcept
noexcept
std::terminate
保持删除器逻辑的简单性: 删除器的主要任务就是释放资源,它不应该包含复杂的业务逻辑、状态管理或者与其他组件的交互。越简单,出错的可能性越小,需要处理异常的情况也就越少。例如,一个负责
delete
fclose
内部消化所有潜在异常: 如果确实无法避免调用可能抛出异常的函数,那么所有这些调用都必须被包裹在
try-catch
catch
std::cerr
考虑资源本身的RAII封装: 有时候,与其在智能指针的删除器里处理复杂逻辑,不如将资源本身封装在一个更小的RAII(Resource Acquisition Is Initialization)类中。这个小RAII类负责资源的构造、销毁以及所有可能抛出异常的中间操作。这样,智能指针的删除器就只需要简单地
delete
// 示例:一个更健壮的资源封装
class FileHandle {
private:
FILE* fp_;
public:
explicit FileHandle(const char* filename, const char* mode) : fp_(nullptr) {
fp_ = fopen(filename, mode);
if (!fp_) {
throw std::runtime_error("Failed to open file: " + std::string(filename));
}
std::cout << "File " << filename << " opened. Handle: " << fp_ << std::endl;
}
// 析构函数处理关闭,内部捕获异常
~FileHandle() noexcept {
if (fp_) {
std::cout << "FileHandle destructor:以上就是C++如何在智能指针中处理自定义删除器异常的详细内容,更多请关注php中文网其它相关文章!
每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号