koturnの日記

転職したい社会人2年生の技術系日記

C++におけるfinallyの実装

はじめに

この記事は世間では十分に議論され尽くしてきたC++におけるfinally句という話について書こうと思う.

C++にはfinally無くて不便ですよね」という言葉は,実際にお仕事をしていて聞くことのある言葉なのだが,やはりC++初心者はfinallyが無い理由を考えないものであるらしい. C++にはRAII(Resource Aquisition Is Initialization)という考え方があり,これはリソース獲得と破棄をctorとdtorを使ってうまくやるというものである.

JavaC#といった言語はGCがあり,ファイナライザやdtorの呼び出しが制御できないようになっている. そのため,わざわざfinally句という枠組みが必要になってしまう. また,同様の理由でtry-with-resource文やusing文といった構文も必要になってしまうわけだ.

しかし,C++のdtorはローカル変数に限れば,その変数の寿命が尽きるとき,すなわちスコープを抜けるときに実行される仕組みとなっている. これがC++がfinally句を必要としない理由である.

ただし,あらゆるリソースの獲得と破棄のコードのためのクラスをいちいち用意していたのでは面倒である. そこで,C++11で取り入れられたラムダ式を利用することにより,より柔軟なRAIIクラスの設計が可能となる.

C++におけるfinallyの実装

以下のコードがfinally句を実現するためのクラスである.

#include <new>
#include <utility>


template<typename F>
class
#if defined(__has_cpp_attribute) && __has_cpp_attribute(nodiscard)
[[nodiscard]]
#elif defined(__GNUC__) && __GNUC_PREREQ(3, 4)
__attribute__((warn_unused_result))
#endif  // defined(__has_cpp_attribute) && __has_cpp_attribute(nodiscard)
Finally
{
public:
  explicit Finally(F&& f) noexcept
    : m_f(std::forward<F>(f))
  {}

  ~Finally()
  {
    m_f();
  }

  Finally(const Finally&) = delete;

  Finally(Finally&&) = default;

  Finally&
  operator=(const Finally&) = delete;

  Finally&
  operator=(Finally&&) = default;

  static void*
  operator new(std::size_t) = delete;

  static void*
  operator new(std::size_t, ...) = delete;

  static void
  operator delete(void*) = delete;

  static void
  operator delete(void*, ...) = delete;

private:
  const F m_f;
};  // class Finally


template<typename F>
static inline Finally<F>
makeFinally(F&& f) noexcept
{
  return Finally<F>{std::forward<F>(f)};
}

テンプレートパラメータの F は戻り値 void(実は何でもよい),引数無しのラムダ式等の関数を想定している. C++14まではクラステンプレートの型推論はできないために,型推論のための関数テンプレート makeFinally を用意しておく. 戻り値の Finally クラスのインスタンスを無視してはならないので,[[nodiscard]] 属性を付与しておく. もし,返り値を無視した場合,関数呼び出し終了直後に指定したラムダが実行されるようになってしまう.

std::function を利用しないのは,std::function にラムダを格納してしまうとインライン展開が行われることが無くなることと,std::functionoperator() 自体の例外チェックコストにより,性能に大きな影響を及ぼすことがあるためだ. ラムダ式ラムダ式のまま保っておくことにより,インライン展開が期待できる. この記事では std::function を用いた実装が紹介されているが,説明のために簡略化したものであり,実際のコードで用いてはならない.

Finally クラスはコピー等を許可する必要が無いため,コピーctorやコピー代入演算子delete 指定しておく. また,new による動的確保は許可しないでおく.

このクラスは以下のように使用する.

#include <iostream>


int
main()
{
  auto f1 = makeFinally([]{
    std::cout << "Hello World! from finally\n";
  });

  {
    std::cout << "Foo\n";
    auto f2 = makeFinally([]{
      std::cout << "Bar\n";
    });
    std::cout << "Baz\n";
  }

  std::cout << "Hello World!\n";
}

このコードの実行結果は以下の通りである.

Foo
Baz
Bar
Hello World!
Hello World! from finally

変数 f1f2 が破棄されるタイミングでそれぞれ指定したラムダが呼び出されていることがわかる.

さて,この記事のタイトルにもあるfinally句としての Finally クラスの使用を限定すると,

try {
  // 処理
} catch (...) {
  // 例外処理
} finally {
  // finally処理
}

と書きたいコードは以下のように書くことができる.

{
  auto f = makeFinally([]{
    // finally処理
  });
  try {
    // 処理
  } catch (...) {
    // 例外処理
  }
}

具体例としては以下のようなコードになるだろうか. (かなり作為的ではあるが)

#include <iostream>
#include <vector>


int
main()
{
  std::vector<int> v{1, 2, 3, 4, 5};
  auto f = makeFinally([&v]{
    std::cout << "vector size: " << v.size() << std::endl;
  });
  try {
    std::cout << "v[0] = " << v.at(0) << "\n";
    std::cout << "v[10] = " << v.at(10) << "\n";  // 例外発生
    std::cout << "end\n";
  } catch (const std::exception& e) {
    std::cerr << e.what() << std::endl;
  }
}

Finally クラスは単にfinally句を実現する以外の用途にも転用可能である. よくあるのがC言語APIで,いくつかの初期化関数と終了処理関数をセットにして呼び出さなければならない場面だ. std::fopen()std::fclose() のような管理ポインタを返す関数であればスマートポインタを利用すればよい話であるが,全てが全てポインタを返却するような関数でないことがある.

例えば,C言語的な関数 init1()close1()init2()close2()init3()close3() が対応しているとしよう. そして,init1()init2()init3() の呼び出しの後に,目的の処理関数 doHoge() を呼び出す場面を考えると以下のようなコードになる.

void
func()
{
  if (init1() != SUCCESS) {
    return;
  }
  if (init2() != SUCCESS) {
    close1();
    return;
  }
  if (init3() != SUCCESS) {
    close2();
    close1();
    return;
  }

  doHoge();

  close3();
  close2();
  close1();
}

Win32 API等のバリバリのC言語関数を扱っているコードにこういうコードが見られることがあるかもしれない.

途中で1つでも失敗すれば,対応する終了処理関数を呼び出さなくてはならないため,同じ終了処理関数呼び出しを並べることになる. 宗教的・盲目的にgotoが禁止されていれば上記のようなコードを書かざるを得ないのである.

これを Finally クラスを用いて書き直すと以下のようになる.

void
func()
{
  if (init1() != SUCCESS) {
    return;
  }
  auto f1 = makeFinally([]{
    close1();
  });

  if (init2() != SUCCESS) {
    return;
  }
  auto f2 = makeFinally([]{
    close2();
  });

  if (init3() != SUCCESS) {
    return;
  }
  auto f3 = makeFinally([]{
    close3();
  });

  doHoge();
}

初期化処理のすぐ後に終了処理関数を書くことができ,途中で return したときの終了処理関数の呼び忘れを心配する必要が無くなる. スッキリとした記述になり,事故防止にもつながるのである. ちなみに,この例でわかる通り,Finally クラスはGoで言うところの defer に相当することがわかると思う.

余談

Finally クラスは継承を用いて実装することもできる.

template<typename F>
class
#if defined(__has_cpp_attribute) && __has_cpp_attribute(nodiscard)
[[nodiscard]]
#elif defined(__GNUC__) && __GNUC_PREREQ(3, 4)
__attribute__((warn_unused_result))
#endif  // defined(__has_cpp_attribute) && __has_cpp_attribute(nodiscard)
Finally
  : private F
{
public:
  explicit Finally(F&& f) noexcept
    : F(std::forward<F>(f))
  {}

  ~Finally()
  {
    F::operator()();
  }

  Finally(const Finally&) = delete;

  Finally(Finally&&) = default;

  Finally&
  operator=(const Finally&) = delete;

  Finally&
  operator=(Finally&&) = default;

  static void*
  operator new(std::size_t) = delete;

  static void*
  operator new(std::size_t, ...) = delete;

  static void
  operator delete(void*) = delete;

  static void
  operator delete(void*, ...) = delete;
};  // class Finally


template<typename F>
static inline Finally<F>
makeFinally(F&& f) noexcept
{
  return Finally<F>{std::forward<F>(f)};
}

ただし,この場合は関数ポインタや final 指定のある関数オブジェクトは指定できない. ラムダとfinal 指定のない関数オブジェクトのみを受け取ることができる.

まぁ,ラムダ式を渡す場合がほとんどだと思うので,この実装でもさほど困らないと思うが.

まとめ

C++はctorとdtorによるRAIIにより,finally句が必要無い. この記事で紹介した Finally クラスは単なるfinally句だけではなく,もっと幅広いリソースの獲得と破棄処理に用いることができる.