C++ 基礎からマスターまで — 第15回
例外が発生してもリソースを安全に管理する
本ブログ作成文は、講義を目指して製作されました。 また、作成された文章の情報も間違っている可能性がありますので、ご注意ください。
ファイルを読み込んでメモリを割り当てた後、中間コードで問題が発生した場合は、すでに取得したリソースをクリーンアップする必要があります。 すべてのreturnと例外パスごとにクリーンアップコードを直接書くことは欠けやすいです。
例外が発生した場合
例外は、現在の場所で処理できない失敗を呼び出し側に渡す方法です。
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が見つかります。 その過程ですでに作成されたローカルオブジェクトのデストラクタが呼び出されます。
すべての失敗に例外が必要なわけではありません。 アイテム検索の失敗のように予想される結果は、optionalまたはboolがより明白になる可能性があります。
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のデストラクタがファイルを閉じます。 動的オブジェクトは unique_ptr、ロックはstd::lock_guard、コンテナメモリはベクトルが同じ役割を果たします。
void UpdateInventory()
{
std::lock_guard<std::mutex> lock(inventoryMutex);
RebuildInventory();
}
RebuildInventoryで例外が発生しても、lockのデストラクタがmutexを解放します。 直接ロックとアンロックを合わせるときに起こりうるミスを減らす。
例外安全性
例外が発生したときに関数がどの状態を保証するかを考えるべきです。
- 基本保証:リソースリークはなく、オブジェクトは利用可能な状態を維持します。
- 強い保証:タスクが失敗した場合は、呼び出し前の状態をそのまま維持します。
- 例外なし保証:関数は例外を送信しません。
新しい状態を一時オブジェクトに完成させた後、最後に交換すれば強い保証を作りやすい。
void Inventory::ReplaceItems(std::vector<Item> newItems)
{
items.swap(newItems);
}
呼び出し引数の作成中に失敗した場合、既存の項目は変わりません。 関数の中では、swapで完成した新しいリストと既存のリストを置き換えます。
noexcept
例外を投げない関数にはnoexceptを表示できます。
Player(Player&& other) noexcept = default;
Player& operator=(Player&& other) noexcept = default;
vectorは、要素を再割り当てするときに安全に移動できるかどうかを判断します。 移動コンストラクタがnoexceptの場合、コピーの代わりに移動を選択する可能性が高いです。
しかし、内部で例外が飛ぶことができますが、noexceptを付けると、例外が外に出る瞬間std::terminateが呼び出されます。 パフォーマンスのためにランダムに付けるキーワードではなく、実際に例外を渡さないという約束だ。
例外の安全性の鍵は、キャッチの多くを書くことではありません。 リソースはRAIIオブジェクトが引き受け、失敗しても既存の状態を壊さない順序でタスクを構成することです。