从 C++ 基础到掌握 — 第 15 集
即使发生异常也能安全地管理资源
该博客上撰写的文章是出于教学目的而创建的。 另外,文章中的信息可能不正确,请谨慎。
如果读取文件并分配内存后中间代码出现问题,则必须清理已经获得的资源。 如果您为每个返回和异常路径手动编写清理代码,则很容易错过它。
如果出现异常
异常是向调用方传达当前位置无法处理的故障的一种方式。
Item LoadItem(int id)
{
if (id <= 0)
{
throw std::invalid_argument("id must be positive");
}
return ReadItemFromFile(id);
}
try
{
Item item = LoadItem(inputId);
}
catch (const std::exception& error)
{
std::cerr << error.what() << '\n';
}
当执行 throw 时,会跳过当前行下面的代码并找到匹配的 catch。 在此过程中,已创建的本地对象的析构函数被调用。
并非所有失败都需要例外。 预期的结果,例如项目搜索失败,作为可选或布尔值可能会更明显。
使用 RAII 进行组织
通过将资源的生命周期与对象的生命周期联系起来,无需分离正常返回路径和异常路径。
std::string ReadConfig(const std::filesystem::path& path)
{
std::ifstream file(path);
if (!file)
{
throw std::runtime_error("config open failed");
}
std::string text(
std::istreambuf_iterator<char>{file},
std::istreambuf_iterator<char>{}
);
return text;
}
即使由于字符串创建在中间失败而发生异常,ifstream 的析构函数也会关闭该文件。 uniqueptr 用于动态对象,uniqueptr 用于锁std::lock_guard,容器内存的作用与向量相同。
void UpdateInventory()
{
std::lock_guard<std::mutex> lock(inventoryMutex);
RebuildInventory();
}
即使 RebuildInventory 中发生异常,锁析构函数也会释放互斥体。 它减少了直接配对锁定和解锁时可能发生的错误。
异常安全
您需要考虑当异常发生时您的函数保证什么状态。
- 基本保证:不存在资源泄漏,对象保持可用。
- 强保证:如果操作失败,保持调用前的状态。
- 保证无异常:该函数不会抛出任何异常。
通过在临时对象中完成新状态并在最后替换它,可以轻松创建强有力的保证。
void Inventory::ReplaceItems(std::vector<Item> newItems)
{
items.swap(newItems);
}
如果创建调用参数时失败,现有项目将不会更改。 在函数内部,通过 swap 完成的新列表替换了现有列表。
noexcept
对于不抛出异常的函数,noexcept可以显示。
Player(Player&& other) noexcept = default;
Player& operator=(Player&& other) noexcept = default;
向量决定了重新分配元素时是否可以安全地移动。 如果移动构造函数是noexcept,那么很可能会选择移动而不是复制。
不过内部可能会出现异常,但是如果加上noexcept的话,异常一出去的瞬间,std::terminate被称为。 它不是为了性能而盲目添加的关键字,而是承诺不会真正抛出异常。
异常安全的关键是不要编写大量的 catch。 资源由 RAII 对象处理,任务按顺序组织,即使失败也不会破坏现有状态。