Szukaj…


Uwagi

RAII oznacza R esource A cquisition I s I nitialization. Czasami nazywany również SBRM (zarządzanie zasobami oparte na zakresie) lub RRID (Resource Release Is Destruction), RAII jest idiomem używanym do wiązania zasobów z czasem życia obiektu. W C ++ niszczyciel dla obiektu zawsze działa, gdy obiekt wykracza poza zakres - możemy to wykorzystać, aby powiązać czyszczenie zasobów z niszczeniem obiektów.

Za każdym razem, gdy musisz zdobyć jakiś zasób (np. Blokadę, uchwyt pliku, przydzielony bufor), który ostatecznie będziesz musiał zwolnić, powinieneś rozważyć użycie obiektu do obsługi tego zarządzania zasobami. Odwijanie stosu nastąpi niezależnie od wyjątku lub wcześniejszego wyjścia z zakresu, więc obiekt obsługi zasobów oczyści zasób, bez konieczności uważnego rozważania wszystkich możliwych bieżących i przyszłych ścieżek kodu.

Warto zauważyć, że RAII nie całkowicie uwalnia twórcę od myślenia o żywotności zasobów. Jednym z przypadków jest oczywiście wywołanie crash lub exit (), które zapobiegnie wywołaniu destruktorów. Ponieważ system operacyjny wyczyści zasoby lokalne procesu, takie jak pamięć po zakończeniu procesu, w większości przypadków nie stanowi to problemu. Jednak w przypadku zasobów systemowych (tj. Nazwanych potoków, plików blokujących, pamięci współużytkowanej) nadal potrzebujesz narzędzi, aby poradzić sobie z przypadkiem, gdy proces nie wyczyścił się sam po sobie, tj. Podczas testu uruchamiania, jeśli plik blokady jest, jeśli tak, sprawdź, czy proces z pid rzeczywiście istnieje, a następnie działaj odpowiednio.

Inna sytuacja ma miejsce, gdy proces uniksowy wywołuje funkcję z rodziny exec, tj. Po fork-exec w celu utworzenia nowego procesu. Tutaj proces potomny będzie miał pełną kopię pamięci rodziców (w tym obiekty RAII), ale po wywołaniu exec nie zostanie wywołany żaden niszczyciel w tym procesie. Z drugiej strony, jeśli proces jest rozwidlony i żaden z procesów nie wywołuje exec, wszystkie zasoby są czyszczone w obu procesach. Jest to poprawne tylko dla wszystkich zasobów, które zostały faktycznie zduplikowane w rozwidleniu, ale w przypadku zasobów systemowych oba procesy będą miały tylko odwołanie do zasobu (tj. Ścieżkę do pliku blokującego) i będą próbowały uwolnić go indywidualnie, potencjalnie powodując drugi proces się nie powiedzie.

Zamykający

Złe blokowanie:

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.
    }
}

To zły sposób na wdrożenie blokowania i odblokowywania muteksu. Aby zapewnić prawidłowe zwolnienie muteksu za pomocą unlock() programista musi upewnić się, że wszystkie przepływy powodujące wyjście z funkcji powodują wywołanie unlock() . Jak pokazano powyżej, jest to proces kruchy, ponieważ wymaga od wszystkich opiekunów ręcznego kontynuowania wykonywania wzorca.

Używając odpowiednio spreparowanej klasy do implementacji RAII, problem jest trywialny:

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 jest niezwykle prostym szablonem klasy, który po prostu wywołuje lock() na argumencie w swoim konstruktorze, zachowuje odniesienie do argumentu i wywołuje unlock() na argumencie w swoim destruktorze. Oznacza to, że gdy lock_guard wykracza poza zasięg, mutex gwarantuje odblokowanie. Nie ma znaczenia, czy przyczyną wykroczenia poza zakres jest wyjątek, czy wczesny powrót - wszystkie sprawy są obsługiwane; bez względu na przepływ sterowania mamy zagwarantowane prawidłowe odblokowanie.

Wreszcie / ScopeExit

W przypadkach, gdy nie chcemy pisać specjalnych klas do obsługi niektórych zasobów, możemy napisać klasę ogólną:

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)}; }

I jego przykładowe użycie

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)`
}

Uwaga (1): Należy rozważyć dyskusję na temat definicji destruktora w celu obsługi wyjątku:

  • ~Finally() noexcept { f(); } : std::terminate jest wywoływane w przypadku wyjątku
  • ~Finally() noexcept(noexcept(f())) { f(); } : terminate () jest wywoływany tylko w przypadku wyjątku podczas rozwijania stosu.
  • ~Finally() noexcept { try { f(); } catch (...) { /* ignore exception (might log it) */} } Nie std::terminate , ale nie możemy obsłużyć błędu (nawet w przypadku braku rozwijania stosu).

ScopeSuccess (c ++ 17)

C ++ 17

Dzięki int std::uncaught_exceptions() możemy zaimplementować akcję, która jest wykonywana tylko w przypadku powodzenia (brak zgłaszanego wyjątku w zakresie). Poprzednio bool std::uncaught_exception() pozwala tylko wykryć, czy działa jakiekolwiek rozwijanie stosu.

#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
    }
}

Wynik:

Success 1

ScopeFail (c ++ 17)

C ++ 17

Dzięki int std::uncaught_exceptions() możemy zaimplementować akcję, która jest wykonywana tylko w przypadku niepowodzenia (zgłoszony wyjątek w zakresie). Poprzednio bool std::uncaught_exception() pozwala tylko wykryć, czy działa jakiekolwiek rozwijanie stosu.

#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
    }
}

Wynik:

Failure 2


Modified text is an extract of the original Stack Overflow Documentation
Licencjonowany na podstawie CC BY-SA 3.0
Nie związany z Stack Overflow