C++
RAII:リソース取得が初期化される
サーチ…
備考
RAIIはcquisitionにI S Iの nitializationをesource Rの略です。 SBRM(Scope-Based Resource Management)またはRRID(Resource Release Is Destruction)とも呼ばれることがありますが、RAIIはリソースをオブジェクトの存続期間に結びつけるために使用されるイディオムです。 C ++では、オブジェクトのデストラクターは、オブジェクトがスコープ外になったときに常に実行されます。これを利用して、リソースのクリーンアップをオブジェクトの破棄に結びつけることができます。
最終的に解放する必要のあるリソース(ロック、ファイルハンドル、割り当て済みバッファなど)を取得する必要がある場合はいつでも、オブジェクトを使用してそのリソース管理を処理することを検討する必要があります。スタックのアンワインディングは、例外または早期スコープの終了に関係なく発生するので、リソースハンドラオブジェクトは、すべての可能な現在および将来のコードパスを慎重に検討することなく、リソースをクリーンアップします。
RAIIがリソースの存続期間を考える開発者を完全に解放しないことは注目に値する。 1つのケースは、明らかに、デストラクタが呼び出されないようにするクラッシュまたはexit()コールです。プロセスの終了後、OSはメモリのようなプロセスローカルリソースをクリーンアップするので、これはほとんどの場合問題ではありません。しかし、システム・リソース(すなわち名前付きパイプ、ロック・ファイル、共有メモリー)では、プロセスがそれ自身の後でクリーンアップしなかった場合、つまりロック・ファイルがある場合はスタートアップ・テストでそれを処理するための機能が必要です。実際にpidを持つプロセスを確認し、それに応じて動作します。
別の状況は、UNIXプロセスがexec-familyから関数を呼び出すとき、つまりfork-execの後に新しいプロセスを作成するときです。ここで、子プロセスは親メモリ(RAIIオブジェクトを含む)のフルコピーを持ちますが、execが呼び出されると、そのプロセスではデストラクタのどれも呼び出されません。一方、プロセスがforkされ、いずれのプロセスもexecを呼び出さない場合は、両方のプロセスですべてのリソースがクリーンアップされます。これは実際にフォークに複製されたすべてのリソースに対してのみ有効ですが、システムリソースでは両方のプロセスがリソースへの参照(ロックファイルへのパス)のみを持ち、個別に解放しようとします他のプロセスは失敗する。
ロッキング
悪いロック:
std::mutex mtx;
void bad_lock_example() {
mtx.lock();
try
{
foo();
bar();
if (baz()) {
mtx.unlock(); // Have to unlock on each exit point.
return;
}
quux();
mtx.unlock(); // Normal unlock happens here.
}
catch(...) {
mtx.unlock(); // Must also force unlock in the presence of
throw; // exceptions and allow the exception to continue.
}
}
これはmutexのロックとロック解除を実装する間違った方法です。 unlock()
使ってミューテックスを正しくリリースするためには、関数を終了したすべてのフローがunlock()
呼び出すことをプログラマが確認する必要があります。上記のように、これは脆弱なプロセスであり、メンテナが手動でパターンを続行する必要があるためです。
適切に細工されたクラスを使用してRAIIを実装すると、問題は簡単です:
std::mutex mtx;
void good_lock_example() {
std::lock_guard<std::mutex> lk(mtx); // constructor locks.
// destructor unlocks. destructor call
// guaranteed by language.
foo();
bar();
if (baz()) {
return;
}
quux();
}
lock_guard
は、非常に単純なクラステンプレートで、コンストラクタ内の引数にlock()
を呼び出し、引数への参照を保持し、そのデストラクタの引数に対してunlock()
を呼び出します。つまり、 lock_guard
が有効範囲外になると、 mutex
ロックが解除されることが保証されます。範囲外になった理由が例外か早期返品であるかどうかは関係ありません。すべてのケースが処理されます。制御フローに関係なく、私たちは正しくロックを解除することを保証しています。
最後に/ ScopeExit
いくつかのリソースを扱う特別なクラスを作成したくない場合は、汎用クラスを書くことができます:
template<typename Function>
class Finally final
{
public:
explicit Finally(Function f) : f(std::move(f)) {}
~Finally() { f(); } // (1) See below
Finally(const Finally&) = delete;
Finally(Finally&&) = default;
Finally& operator =(const Finally&) = delete;
Finally& operator =(Finally&&) = delete;
private:
Function f;
};
// Execute the function f when the returned object goes out of scope.
template<typename Function>
auto onExit(Function &&f) { return Finally<std::decay_t<Function>>{std::forward<Function>(f)}; }
そしてその使用例
void foo(std::vector<int>& v, int i)
{
// ...
v[i] += 42;
auto autoRollBackChange = onExit([&](){ v[i] -= 42; });
// ... code as recursive call `foo(v, i + 1)`
}
注(1):例外を処理するためには、デストラクタ定義についての議論が必要です。
-
~Finally() noexcept { f(); }
:例外の場合にstd::terminate
が呼び出されます -
~Finally() noexcept(noexcept(f())) { f(); }
:terminate()は、スタックの巻き戻し中に例外が発生した場合にのみ呼び出されます。 -
~Finally() noexcept { try { f(); } catch (...) { /* ignore exception (might log it) */} }
std::terminate
呼び出されませんが、エラーを処理することはできません(非スタックの巻き戻しの場合でも)。
ScopeSuccess(c ++ 17)
int std::uncaught_exceptions()
おかげで、成功したときにのみ実行されるアクション(スコープ内にスローされた例外はありませんint std::uncaught_exceptions()
を実装できます。これまで、 bool std::uncaught_exception()
は、スタックアンワインドが実行されているかどうかを検出するだけです。
#include <exception>
#include <iostream>
template <typename F>
class ScopeSuccess
{
private:
F f;
int uncaughtExceptionCount = std::uncaught_exceptions();
public:
explicit ScopeSuccess(const F& f) : f(f) {}
ScopeSuccess(const ScopeSuccess&) = delete;
ScopeSuccess& operator =(const ScopeSuccess&) = delete;
// f() might throw, as it can be caught normally.
~ScopeSuccess() noexcept(noexcept(f())) {
if (uncaughtExceptionCount == std::uncaught_exceptions()) {
f();
}
}
};
struct Foo {
~Foo() {
try {
ScopeSuccess logSuccess{[](){std::cout << "Success 1\n";}};
// Scope succeeds,
// even if Foo is destroyed during stack unwinding
// (so when 0 < std::uncaught_exceptions())
// (or previously std::uncaught_exception() == true)
} catch (...) {
}
try {
ScopeSuccess logSuccess{[](){std::cout << "Success 2\n";}};
throw std::runtime_error("Failed"); // returned value
// of std::uncaught_exceptions increases
} catch (...) { // returned value of std::uncaught_exceptions decreases
}
}
};
int main()
{
try {
Foo foo;
throw std::runtime_error("Failed"); // std::uncaught_exceptions() == 1
} catch (...) { // std::uncaught_exceptions() == 0
}
}
出力:
Success 1
ScopeFail(c ++ 17)
int std::uncaught_exceptions()
おかげで、失敗(スコープでスローされた例外)でのみ実行されるアクションを実装できます。これまで、 bool std::uncaught_exception()
は、スタックアンワインドが実行されているかどうかを検出するだけです。
#include <exception>
#include <iostream>
template <typename F>
class ScopeFail
{
private:
F f;
int uncaughtExceptionCount = std::uncaught_exceptions();
public:
explicit ScopeFail(const F& f) : f(f) {}
ScopeFail(const ScopeFail&) = delete;
ScopeFail& operator =(const ScopeFail&) = delete;
// f() should not throw, else std::terminate is called.
~ScopeFail() {
if (uncaughtExceptionCount != std::uncaught_exceptions()) {
f();
}
}
};
struct Foo {
~Foo() {
try {
ScopeFail logFailure{[](){std::cout << "Fail 1\n";}};
// Scope succeeds,
// even if Foo is destroyed during stack unwinding
// (so when 0 < std::uncaught_exceptions())
// (or previously std::uncaught_exception() == true)
} catch (...) {
}
try {
ScopeFail logFailure{[](){std::cout << "Failure 2\n";}};
throw std::runtime_error("Failed"); // returned value
// of std::uncaught_exceptions increases
} catch (...) { // returned value of std::uncaught_exceptions decreases
}
}
};
int main()
{
try {
Foo foo;
throw std::runtime_error("Failed"); // std::uncaught_exceptions() == 1
} catch (...) { // std::uncaught_exceptions() == 0
}
}
出力:
Failure 2