C++にfinally?
昨晩ついったーでもつぶやいたのですが、
C++とfinally - 株式会社CFlatの明後日スタイルのブログ
このブログ記事を読んだ感想は、C++にfinallyは特にいらないんじゃないかなー、でした。
1. リソース管理するならRAII使うほうがいいと思う
関数の実行時にどのような例外が発生しても、プログラムを正しく矛盾のない状態に保つことを例外安全といいます。
例外安全 - プログラミング言語 D (日本語訳)
例外処理 - Wikipedia
Exceptional C++―47のクイズ形式によるプログラム問題と解法 (C++ in‐Depth Series)
- 作者: ハーブサッター,浜田光之,Harb Sutter,浜田真理
- 出版社/メーカー: ピアソンエデュケーション
- 発売日: 2000/11
- メディア: 単行本
- 購入: 9人 クリック: 134回
- この商品を含むブログ (63件) を見る
先のブログでは、finallyの利点として、newで確保したメモリを例外発生時にも例外が発生しない時にも正しく解放するという例外安全の処理が、finallyを使用するとすっきりと書けるという風に述べられています。
C++ではリソースを管理し、例外安全を実現するためにRAIIというIdiomを使用します。
RAIIとはResource Acquisition Is Initializationの頭文字で、オブジェクトの初期化の際にリソースを確保し、デストラクタでそのリソースを解放する仕組みです。
先のブログで使用しているstd::unique_ptrは、メモリというリソースの管理にRAIIを用いた、スマートポインタと呼ばれる種類のクラスです。
スマートポインタのデストラクタがメモリを自動で解放するため、例外発生時にも例外が発生しない時にもリソース管理と例外安全の処理を明示的に書く必要がなくなります。
先のブログで例外安全の処理のために、finallyの代替手段としてRAII(スマートポインタ)を使った例が載せられていますが、
(コード引用)
try { std::unique_ptr<int[]> p(new int[10]) ; std::cout << "例外を投げるかもしれない処理" << std::endl ; if (condition) return ; if (need_goto) goto LABEL ; ... } catch (std::exception e) { std::cout << "Caught a std::exception!" << std::endl ; } catch (...) { std::cout << "Caught an unexpected exception!" << std::endl ; throw ; } ... LABEL : ...
finallyを使ったコードよりもすっきり書けているのではないでしょうか。
メモリ以外でRAIIが有効な場面としては、Mutexの管理などがあります。これはMutexの所有権をリソースとして、その管理にRAIIを用いるものです。
#include <iostream> #include <mutex> //標準スレッドはC++11から。C++03環境ではBoost.Threadを使用する #include <thread> //標準スレッドはC++11から。C++03環境ではBoost.Threadを使用する std::mutex mtx; //Mutexを表すクラス。Mutexの所有権を取得するlock()メンバ関数, 所有権を解放するunlock()メンバ関数を持つ int count = 0; void foo() { for(int i = 0; i < 100; ++i) { std::lock_guard<std::mutex> lock(mtx); //このスコープ内だけMutexを取得する count += 1; } //for文の先頭に戻る時やfor文を抜ける際にはMutexを解放する } void bar() { for(int i = 0; i < 100; ++i) { std::lock_guard<std::mutex> lock(mtx); //このスコープ内だけMutexを取得する count += 1; } //for文の先頭に戻る時やfor文を抜ける際にはMutexを解放する } int main() { std::thread th1(foo); //foo関数を別スレッドで起動 std::thread th2(bar); //bar関数を別スレッドで起動 th1.join(); //スレッドの終了を待機 th2.join(); //スレッドの終了を待機 std::cout << count << std::endl; }
ここではRAIIによるロックの管理にstd::lock_guardクラス(クラステンプレート)を使用しています。
std::lock_guardクラスはテンプレート引数に、BasicLockableコンセプトを満たすクラス、すなわちlock()メンバ関数とunlock()メンバ関数を持つクラスを指定します。
std::lock_guardクラスのオブジェクトを初期化する際に、テンプレート引数で指定したクラスのオブジェクトをコンストラクタに渡すと、コンストラクタ内で受け取ったオブジェクトのlock()メンバ関数を呼び出してMutexの所有権を確保し、逆にstd::lock_guardクラスのオブジェクトが破棄される際には、デストラクタでunlock()メンバ関数を呼び出してMutexの所有権を解放します。
このようにRAIIを用いることで、「Mutexの所有権を適宜取得し過不足なく解放する」という煩雑な処理を、単にオブジェクトの寿命として管理できるため、ひとつのMutexの所有権を、明示的に手続き的に取得と解放を対応付けて管理する必要がなくなります。
またファイルストリームもRAIIの良い例です。std::fstreamクラスのオブジェクトを初期化する際にコンストラクタにファイル名を渡すと、内部でそのファイルを開き、オブジェクトが破棄される際にはファイルが自動で閉じられる様になっています。
そして、RAIIはtry-catch句とは独立した仕組みであるため、例外と関係なく、以下のような適当な場所で関数から戻りたいという状況でも使用できます。
void foo(int condition) { std::unique_ptr<SomeClass> p(new SomeClass); if(condition == CONDITION_A) { doSomething(p); return; } else if(condition == CONDITION_B || condtion == CONDITION_C) { bool result = doSomethingDifferent(p); if(result) { return; } doSomethingDifferentDifferent(result); } else { doSomethingDifferentDifferentDifferent(p); } }
2. スコープ抜けるときになにか処理させたいならScopeExit的な仕組み使うほうがいいと思う
ScopeExitはスコープを抜ける際に指定した処理を行うための仕組みです。
D言語では言語機能に含まれ、C++ではBoost.ScopeExitなどを用いて実現できます(多少不恰好ですが)
#include <iostream> #include <boost/scope_exit.hpp> void foo() { BOOST_SCOPE_EXIT(void) { std::cout << "try{}が終わろうともreturnされようともthrowされようとも必ず実行する処理" << std::endl; } BOOST_SCOPE_EXIT_END try { std::cout << "例外を投げるかもしれない処理" << std::endl; return; } catch (...) { std::cout << "例外が発生した!" << std::endl; throw; } } int main() { foo(); }
finallyとScopeExitを比較すると、ScopeExitでは、try-catch句よりも先に後処理を書く必要があります。
finallyを使用した場合は、(例外が投げられようと、投げられまいと、)最終的に行いたい処理を、関数の最後にまとめかけるため、処理の流れが直感的になるというのが利点かもしれません。
しかし、必ずしも最後に書くことが最良ではない状況もあると思います。先のブログで書かれていたように、「間の処理が長くなると初期化処理と後始末がコード上で離れていき、わけがわからなくなる心配が」あるためです。
ScopeExitはスコープからから抜ける際に処理が行われるという意図が明確であるため、関数内で行われた何らかの前処理と組み合わせて使用することで、前処理と対応する後処理を明確に表現することができます。
例外安全 - プログラミング言語 D (日本語訳)
こちらのページの言葉を借りれば、
密接に関連した処理は一カ所にまとまっているべきです。
というわけです。
また、ScopeExitはfinallyよりも汎用的です。
try句あるいはtry-catch句に付随して使用しなければならないfinallyに対し、ScopeExitはRAIIの場合と同じようにtry句やtry-catch句とは独立した仕組みであるため、それら例外処理に関する機能とは関係なく使用できます。
例えば以下のように、適当な場所で関数から戻りつつ、関数から戻る際に行いたい処理を明示的に表すためにScopeExitを使用することもできます。
void foo(int condition) { int result = 0; //! 関数を抜ける際にログを吐くようにする BOOST_SCOPE_EXIT((condition)(&result)) { time_t const now = time(0); doLogging(now, condition, result); } BOOST_SCOPE_EXIT_END if(condition == CONDITION_A) { result = doSomething(condition); return; } else if(condition == CONDITION_B || condition == CONDITION_C) { result = doSomethingDifferent(condition); if(result != 0) { return; } result = doSomethingDifferentDifferent(condition); } else { result = doSomethingDifferentDifferentDifferent(condition); } } // 例外に関係なく、関数から抜ける際にログが吐き出される
まとめ
finallyは、例外安全を実現するためとスコープを抜ける際に必ず行う処理のためと両方の場面で使用されますが、リソース管理にはRAIIを、スコープを抜ける際の処理にはScopeExitを使用すると、コード上で意図がより明確になり、finallyよりもわかりやすいように思います。
このように、C++ではリソースの管理や例外安全性、スコープを抜ける際の処理について、finallyとは別の方法で、よりコードの意図を明確にする仕組みがあるため、finallyの必要性は低いんじゃないかなーと思いました。