スコープを抜けるときに必ず実行してほしい処理を仕込むイディオム

に公開

今日は C++ の標準ライブラリにはない機能について書こうと思います。Java を使ったことがある人は以下のtry-catch-finallyというパターンを知っていると思います。

try
{
  // まず実行するコード
}
catch(err)
{
  // エラーの場合に実行するコード
}
finally
{
  // エラーでも成功でも最後に絶対に実行するコード
}

こちらのfinallyに似ているため、finallyと呼ばれている機能になります。ScopeGuardと呼ばれることもあります。

要するに、関数などのスコープの中で、何かをやってみて、成功してもエラーになっても例外が投げられても絶対に最後に実行したいコードがある場合に使います。何か絶対にしないといけない後処理やクリーンアップがある時とかは便利ですね。

そういう場合は、C++ ではクラスを作って、そのコードをそのクラスのデストラクタの中で実行すると思いますが、毎回クラスを作るのは面倒なので、それをやってくれる以下のクラスと関数を準備しておきます。

template <class F>
class final_action
{
public:
  explicit final_action(const F& ff) noexcept : f{ff} {}
  explicit final_action(F&& ff) noexcept : f{std::move(ff)} {}
  
  ~final_action() noexcept { if (invoke) f(); }
  
  final_action(final_action&& other) noexcept
    : f(std::move(other.f)), invoke(std::exchange(other.invoke, false))
  {}
  
  final_action(const final_action&)   = delete;
  void operator=(const final_action&) = delete;
  void operator=(final_action&&)      = delete;
  
private:
  F f;
  bool invoke = true;
};

template <class F>
[[nodiscard]] auto finally(F&& f) noexcept
{
  return final_action<std::decay_t<F>>{std::forward<F>(f)};
}

こちらは Microsoft の GSL (Guidelines Support Library) の実装になります。

  • final_actionのインスタンスを作って、関数を渡すと、そのインスタンスが破棄される時に絶対にその関数をデストラクタの中で実行してくれます。
  • finallyfinal_actionを作ってくれるユーティリティ関数です。

実際に使う時は、以下のように使います。

int parseFile()
{
  std::ofstream file;
  file.open ("file.xml");
  
  auto final_close = finally([&]() { file.close(); });
  
  // ここから何があっても、このスコープから出る時に絶対に file.close() が実行されます。
}

ちなみに言語自体に同等の機能を持っている言語もありますね。go言語だとdeferなんかがそうだと思います。

func main() {
  f, err := os.Create("file")
  if err != nil {
    panic("error")
  }
  defer f.Close() // スコープを抜けるときに必ずClose()してくれる

  do_something()
  // 変数fのスコープを抜けるのでf.Close()してくれる
}

finallyは場合によってgotoの代わりに使えます。例えば、以下のgotoを使うコードがあるとしましょう。

bool connect() {
  if (!initialize_connection()) {
    return false;
  }
  if (!authenticate()) {
    goto cleanup;
  }
  if (!setup_session()) {
    goto cleanup;
  }
  return true;
  
cleanup:
  rollback_connection();
  return false;
}

finallyを使えば以下のように書けます。

bool connect() {
  if (!initialize_connection()) {
    return false;
  }

  auto final_rollback = finally([&]() { rollback_connection(); });

  if (!authenticate()) {
    return false;
  }
  if (!setup_session()) {
    return false;
  }

  return true;
}

注意しないといけないのは、finallyに渡す関数に戻り値がある場合は、その戻り値を取得できないことです。なので、戻り値がない関数か、戻り値を参照しない関数でないといけないです。

非常に便利な関数なので、皆様も使ってみてください。


|cpp記事一覧へのリンク|

Discussion