Suche…


Bemerkungen

RAII steht für R eSource Ü bernahme I s I nitialization. RAII, gelegentlich auch als SBRM (Scope-Based Resource Management) oder RRID (Resource Release Is Destruction) bezeichnet, ist ein Idiom, mit dem Ressourcen an die Lebensdauer des Objekts gebunden werden. In C ++ wird der Destruktor für ein Objekt immer ausgeführt, wenn ein Objekt seinen Gültigkeitsbereich verlässt - wir können dies nutzen, um die Ressourcenbereinigung mit der Objektzerstörung zu verknüpfen.

Jedes Mal, wenn Sie eine Ressource (z. B. eine Sperre, ein Dateihandle, einen zugewiesenen Puffer) benötigen, die Sie eventuell freigeben müssen, sollten Sie ein Objekt zur Verwaltung dieser Ressourcenverwaltung in Betracht ziehen. Das Abwickeln des Stapels wird unabhängig von der Ausnahme oder dem vorzeitigen Beenden des Bereichs ausgeführt. Daher bereinigt das Ressourcenhandlerobjekt die Ressource für Sie, ohne dass Sie alle möglichen aktuellen und zukünftigen Codepfade sorgfältig prüfen müssen.

Es ist erwähnenswert, dass RAII den Entwickler nicht völlig frei macht, über die Lebensdauer von Ressourcen nachzudenken. Ein Fall ist offensichtlich ein Absturz- oder Beendigungsaufruf (), der den Aufruf von Destruktoren verhindert. Da das Betriebssystem nach dem Beenden eines Prozesses prozesslokale Ressourcen wie Speicher bereinigt, ist dies in den meisten Fällen kein Problem. Bei Systemressourcen (z. B. Named Pipes, Sperrdateien, Shared Memory) benötigen Sie jedoch noch Einrichtungen, um den Fall zu bewältigen, dass ein Prozess nicht nach sich selbst bereinigt wird, dh beim Starttest, ob die Sperrdatei vorhanden ist. Überprüfen Sie, ob der Prozess tatsächlich vorhanden ist, und handeln Sie entsprechend.

Eine andere Situation ist, wenn ein Unix-Prozess eine Funktion aus der Exec-Familie aufruft, dh nach einem Fork-Exec, um einen neuen Prozess zu erstellen. Der untergeordnete Prozess verfügt über eine vollständige Kopie des übergeordneten Speichers (einschließlich der RAII-Objekte). Sobald exec jedoch aufgerufen wurde, wird keiner der Destruktoren in diesem Prozess aufgerufen. Wenn dagegen ein Prozess verzweigt ist und keiner der Prozesse exec aufgerufen hat, werden alle Ressourcen in beiden Prozessen bereinigt. Dies gilt nur für alle Ressourcen, die tatsächlich in der Verzweigung dupliziert wurden. Bei Systemressourcen haben beide Prozesse jedoch nur einen Verweis auf die Ressource (dh den Pfad zu einer Sperrdatei). Beide versuchen, sie einzeln freizugeben, was möglicherweise dazu führt der andere Prozess zum Scheitern verurteilt.

Sperren

Schlechte Verriegelung:

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

Dies ist der falsche Weg, um das Verriegeln und Entriegeln des Mutex zu implementieren. Um die korrekte Freigabe des Mutex mit unlock() sicherzustellen, muss der Programmierer sicherstellen, dass alle Flüsse, die zum Beenden der Funktion führen, einen Aufruf von unlock() . Wie oben gezeigt, ist dies ein spröder Prozess, da alle Betreuer dem Muster manuell folgen müssen.

Bei Verwendung einer entsprechend gestalteten Klasse zur Implementierung von RAII ist das Problem trivial:

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 ist eine extrem einfache Klassenvorlage, die einfach lock() für ihr Argument in ihrem Konstruktor aufruft, einen Verweis auf das Argument lock_guard unlock() für das Argument in seinem Destruktor aufruft. Das heißt, wenn der lock_guard den lock_guard , ist die lock_guard des mutex garantiert. Es spielt keine Rolle, ob der Ausnahmefall eine Ausnahme oder eine vorzeitige Rückgabe ist - alle Fälle werden behandelt. Unabhängig vom Kontrollfluss haben wir garantiert, dass wir die Entriegelung korrekt durchführen.

Schließlich / ScopeExit

Für den Fall, dass wir keine speziellen Klassen schreiben wollen, um eine Ressource zu behandeln, schreiben wir möglicherweise eine generische Klasse:

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

Und sein Beispielgebrauch

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

Anmerkung (1): Einige Diskussionen über die Definition von Destruktoren müssen berücksichtigt werden, um eine Ausnahme zu behandeln:

  • ~Finally() noexcept { f(); } : std::terminate wird im Ausnahmefall aufgerufen
  • ~Finally() noexcept(noexcept(f())) { f(); } : terminate () wird nur im Ausnahmefall beim Stack-Abwickeln aufgerufen.
  • ~Finally() noexcept { try { f(); } catch (...) { /* ignore exception (might log it) */} } Kein std::terminate aufgerufen, aber Fehler können nicht behandelt werden (auch nicht für das Abwickeln ohne Stack).

ScopeSuccess (c ++ 17)

C ++ 17

Dank int std::uncaught_exceptions() können wir eine Aktion implementieren, die nur bei Erfolg ausgeführt wird (keine ausgelöste Ausnahme im Gültigkeitsbereich). Bisher konnte bool std::uncaught_exception() nur feststellen, ob ein Stack-Abwickelvorgang ausgeführt wird.

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

Ausgabe:

Success 1

ScopeFail (c ++ 17)

C ++ 17

Dank int std::uncaught_exceptions() können wir eine Aktion implementieren, die nur bei einem Fehler ausgeführt wird (Ausnahme im Geltungsbereich ausgelöst). Bisher konnte bool std::uncaught_exception() nur feststellen, ob ein Stack-Abwickelvorgang ausgeführt wird.

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

Ausgabe:

Failure 2


Modified text is an extract of the original Stack Overflow Documentation
Lizenziert unter CC BY-SA 3.0
Nicht angeschlossen an Stack Overflow