はじめに
この記事は世間では十分に議論され尽くしてきたC++におけるfinally句という話について書こうと思う.
「C++にはfinally無くて不便ですよね」という言葉は,実際にお仕事をしていて聞くことのある言葉なのだが,やはりC++初心者はfinallyが無い理由を考えないものであるらしい. C++にはRAII(Resource Aquisition Is Initialization)という考え方があり,これはリソース獲得と破棄をctorとdtorを使ってうまくやるというものである.
JavaやC#といった言語はGCがあり,ファイナライザやdtorの呼び出しが制御できないようになっている. そのため,わざわざfinally句という枠組みが必要になってしまう. また,同様の理由でtry-with-resource文やusing文といった構文も必要になってしまうわけだ.
しかし,C++のdtorはローカル変数に限れば,その変数の寿命が尽きるとき,すなわちスコープを抜けるときに実行される仕組みとなっている. これがC++がfinally句を必要としない理由である.
ただし,あらゆるリソースの獲得と破棄のコードのためのクラスをいちいち用意していたのでは面倒である. そこで,C++11で取り入れられたラムダ式を利用することにより,より柔軟なRAIIクラスの設計が可能となる.
C++におけるfinallyの実装
以下のコードがfinally句を実現するためのクラスである.
#include <new> #include <type_traits> #include <utility> template <typename F> class #if defined(__has_cpp_attribute) && __has_cpp_attribute(nodiscard) [[nodiscard]] #endif // defined(__has_cpp_attribute) && __has_cpp_attribute(nodiscard) Finally final { private: #if __cplusplus >= 201703L || defined(_MSVC_LANG) && _MSVC_LANG >= 201703L static_assert(std::is_invocable_v<F>, "[Finally] The finally function must be callable with no arguments."); #else struct is_invocable_impl { template < typename T, typename... Args > static auto check(T&& obj, Args&&... args) -> decltype(obj(args...), std::true_type{}); template <typename...> static auto check(...) -> std::false_type; }; // struct is_invocable_impl template < typename T, typename... Args > struct is_invocable : public decltype(is_invocable_impl::check(std::declval<T>(), std::declval<Args>()...)) {}; // struct is_invocable static_assert(is_invocable<F>::value, "[Finally] The finally function must be callable with no arguments."); #endif // __cplusplus >= 201703L || defined(_MSVC_LANG) && _MSVC_LANG >= 201703L public: template <typename G> explicit Finally(G&& g) #if defined(__cplusplus) && __cplusplus >= 201103L \ || defined(_MSVC_LANG) && _MSVC_LANG >= 201103L \ || defined(_MSC_VER) && (_MSC_VER > 1800 || (_MSC_VER == 1800 && _MSC_FULL_VER == 180021114)) noexcept #else throw() #endif : m_f{std::forward<G>(g)} {} ~Finally() { m_f(); } Finally(const Finally&) = delete; Finally& operator=(const Finally&) = delete; Finally(Finally&&) = delete; Finally& operator=(Finally&&) = delete; template <typename... Args> static void* operator new(std::size_t, Args&&...) = delete; template <typename... Args> static void operator delete(void*, Args&&...) = delete; private: const F m_f; }; // class Finally #if defined(__cpp_deduction_guides) template <typename F> Finally(F&&) -> Finally<std::decay_t<F>>; #endif // defined(__cpp_deduction_guides) namespace { template <typename F> #if !defined(__has_cpp_attribute) || !__has_cpp_attribute(nodiscard) # if defined(__GNUC__) && (__GNUC__ > 3 || __GNUC__ == 3 && __GNUC_MINOR__ >= 4) __attribute__((warn_unused_result)) # elif defined(_MSC_VER) && _MSC_VER >= 1700 && defined(_Check_return_) _Check_return_ # endif // defined(__GNUC__) && (__GNUC__ > 3 || __GNUC__ == 3 && __GNUC_MINOR__ >= 4) #endif // !defined(__has_cpp_attribute) || !__has_cpp_attribute(nodiscard) inline Finally<typename std::decay<F>::type> makeFinally(F&& f) #if defined(__cplusplus) && __cplusplus >= 201103L \ || defined(_MSVC_LANG) && _MSVC_LANG >= 201103L \ || defined(_MSC_VER) && (_MSC_VER > 1800 || (_MSC_VER == 1800 && _MSC_FULL_VER == 180021114)) noexcept #else throw() #endif { return Finally<typename std::decay<F>::type>{std::forward<typename std::decay<F>::type>(f)}; } } // namespace
テンプレートパラメータの F
は戻り値 void
(実は何でもよい),引数無しのラムダ式等の関数を想定している.
C++14まではクラステンプレートの型推論はできないために,型推論のための関数テンプレート makeFinally
を用意しておく.
戻り値の Finally
クラスのインスタンスを無視してはならないので,[[nodiscard]]
属性を付与しておく.
もし,返り値を無視した場合,関数呼び出し終了直後に指定したラムダが実行されるようになってしまう.
std::function
を利用しないのは,std::function
にラムダを格納してしまうとインライン展開が行われることが無くなることと,std::function
の operator()
自体の例外チェックコストにより,性能に大きな影響を及ぼすことがあるためだ.
ラムダ式をラムダ式のまま保っておくことにより,インライン展開が期待できる.
この記事では 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
変数 f1
,f2
が破棄されるタイミングでそれぞれ指定したラムダが呼び出されていることがわかる.
さて,この記事のタイトルにもある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
クラスは継承を用いて実装することもできる.
#include <new> #include <type_traits> #include <utility> template <typename F> class #if defined(__has_cpp_attribute) && __has_cpp_attribute(nodiscard) [[nodiscard]] #endif // defined(__has_cpp_attribute) && __has_cpp_attribute(nodiscard) Finally final : private F { private: #if __cplusplus >= 201703L || defined(_MSVC_LANG) && _MSVC_LANG >= 201703L static_assert(std::is_invocable_v<F>, "[Finally] The finally function must be callable with no arguments."); #else struct is_invocable_impl { template < typename T, typename... Args > static auto check(T&& obj, Args&&... args) -> decltype(obj(args...), std::true_type{}); template <typename...> static auto check(...) -> std::false_type; }; // struct is_invocable_impl template < typename T, typename... Args > struct is_invocable : public decltype(is_invocable_impl::check(std::declval<T>(), std::declval<Args>()...)) {}; // struct is_invocable static_assert(is_invocable<F>::value, "[Finally] The finally function must be callable with no arguments."); #endif // __cplusplus >= 201703L || defined(_MSVC_LANG) && _MSVC_LANG >= 201703L public: template <typename G> explicit Finally(G&& g) #if defined(__cplusplus) && __cplusplus >= 201103L \ || defined(_MSVC_LANG) && _MSVC_LANG >= 201103L \ || defined(_MSC_VER) && (_MSC_VER > 1800 || (_MSC_VER == 1800 && _MSC_FULL_VER == 180021114)) noexcept #else throw() #endif : F{std::forward<G>(g)} {} ~Finally() { F::operator()(); } Finally(const Finally&) = delete; Finally& operator=(const Finally&) = delete; Finally(Finally&&) = delete; Finally& operator=(Finally&&) = delete; template <typename... Args> static void* operator new(std::size_t, Args&&...) = delete; template <typename... Args> static void operator delete(void*, Args&&...) = delete; }; // class Finally #if defined(__cpp_deduction_guides) template <typename F> Finally(F&&) -> Finally<std::decay_t<F>>; #endif // defined(__cpp_deduction_guides) namespace { template <typename F> #if !defined(__has_cpp_attribute) || !__has_cpp_attribute(nodiscard) # if defined(__GNUC__) && (__GNUC__ > 3 || __GNUC__ == 3 && __GNUC_MINOR__ >= 4) __attribute__((warn_unused_result)) # elif defined(_MSC_VER) && _MSC_VER >= 1700 && defined(_Check_return_) _Check_return_ # endif // defined(__GNUC__) && (__GNUC__ > 3 || __GNUC__ == 3 && __GNUC_MINOR__ >= 4) #endif // !defined(__has_cpp_attribute) || !__has_cpp_attribute(nodiscard) inline Finally<typename std::decay<F>::type> makeFinally(F&& f) #if defined(__cplusplus) && __cplusplus >= 201103L \ || defined(_MSVC_LANG) && _MSVC_LANG >= 201103L \ || defined(_MSC_VER) && (_MSC_VER > 1800 || (_MSC_VER == 1800 && _MSC_FULL_VER == 180021114)) noexcept #else throw() #endif { return Finally<typename std::decay<F>::type>{std::forward<typename std::decay<F>::type>(f)}; } } // namespace
ただし,この場合は関数ポインタや final
指定のある関数オブジェクトは指定できない.
ラムダとfinal
指定のない関数オブジェクトのみを受け取ることができる.
まぁ,ラムダ式を渡す場合がほとんどだと思うので,この実装でもさほど困らないと思うが.
まとめ
C++はctorとdtorによるRAIIにより,finally句が必要無い.
この記事で紹介した Finally
クラスは単なるfinally句だけではなく,もっと幅広いリソースの獲得と破棄処理に用いることができる.