C++
RAII : 자원 획득 초기화
수색…
비고
RAII는 cquisition에게 I의 I의 nitialization을 esource R을 의미합니다. 때때로 SBRM (Scope-Based Resource Management) 또는 RRID (Resource Release Is Destruction)라고도하는 RAII는 리소스를 개체 수명에 연결하는 데 사용되는 관용구입니다. C ++에서 객체의 소멸자는 객체가 범위를 벗어날 때 항상 실행됩니다. 따라서 객체 정리에 리소스 정리를 묶을 수 있습니다.
최종적으로 해제해야하는 리소스 (예 : 잠금, 파일 핸들, 할당 된 버퍼)를 확보해야 할 때마다 개체를 사용하여 리소스 관리를 처리하는 것이 좋습니다. 스택 unwinding은 예외 또는 초기 스코프 종료에 관계없이 발생하므로 리소스 핸들러 객체는 가능한 모든 현재 및 미래 코드 경로를 신중하게 고려하지 않고 리소스를 정리합니다.
RAII가 자원의 수명에 대해 생각하는 개발자를 완전히 자유롭게하지는 않는다는 점은 주목할 가치가 있습니다. 한 가지 경우는 소멸자가 호출되지 않도록하는 crash 또는 exit () 호출입니다. OS는 프로세스가 끝난 후 메모리와 같은 프로세스 로컬 리소스를 정리하기 때문에 대부분의 경우 문제가되지 않습니다. 그러나 시스템 리소스 (즉, 명명 된 파이프, 잠금 파일, 공유 메모리)를 사용하면 프로세스가 자체적으로 정리하지 않은 경우 (즉, 잠금 파일이있는 경우 시작 테스트시)를 처리 할 수있는 기능이 여전히 필요합니다. pid가있는 프로세스가 실제로 존재하는지 확인한 다음 그에 따라 작동하십시오.
또 다른 상황은 유닉스 프로세스가 exec-family로부터 함수를 호출 할 때, 즉 fork-exec 후에 새로운 프로세스를 생성하는 경우이다. 여기서 자식 프로세스는 부모 메모리 (RAII 객체 포함)의 전체 사본을 가지지 만 exec가 호출되면 해당 프로세스에서 소멸자가 호출되지 않습니다. 반면에 프로세스가 분기되고 프로세스가 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.
}
}
이것은 뮤텍스의 잠금과 잠금 해제를 구현하는 잘못된 방법입니다. 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
우리가 어떤 리소스를 다루기 위해 특별한 클래스를 작성하고 싶지 않을 때, 우리는 generic 클래스를 작성할 수있다 :
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()
덕택에 성공시에만 실행되는 액션을 구현할 수 있습니다 (범위에서 예외가 발생하지 않음). 이전 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