Μετάβαση στο περιεχόμενο

RAII: The Fundamental Resource Pattern

Learn RAII (Resource Acquisition Is Initialization) — the core C++ pattern for exception-safe resource management. Implement RAII wrappers, understand lock_guard and scoped_lock, and learn why RAII types must be move-only or ref-counted.

RAII: Acquire in Constructor, Release in Destructor

Το RAII (Resource Acquisition Is Initialization) είναι το πιο σημαντικό μοτίβο στο C++. Η ιδέα είναι απλή:

1. Αποκτήστε έναν πόρο στον κατασκευαστή (άνοιγμα αρχείου, κλείδωμα ενός mutex, εκχώρηση μνήμης, σύνδεση σε μια βάση δεδομένων).
2. Απελευθερώστε τον πόρο στον καταστροφέα (κλείστε το αρχείο, ξεκλειδώστε το mutex, ελευθερώστε τη μνήμη, αποσυνδέστε).

Επειδή το C++ εγγυάται ότι οι καταστροφείς καλούνται όταν ένα αντικείμενο ξεφεύγει από το πεδίο εφαρμογής — ακόμα και αν υπάρχει εξαίρεση — το RAII κάνει τη διαχείριση πόρων αυτόματη και ασφαλή για εξαιρέσεις. Ποτέ δεν ξεχνάς να απελευθερώσεις, και ποτέ δεν κυκλοφορείς δύο φορές.

Κάθε καλά σχεδιασμένος τύπος C++ που διαχειρίζεται έναν πόρο χρησιμοποιεί RAII:std::string,std::vector,std::unique_ptr,std::fstream,std::lock_guard,std::unique_lock.

Implementing an RAII Wrapper

Here is a complete RAII wrapper for a file descriptor (POSIX). It acquires in the constructor, releases in the destructor, and supports move semantics for ownership transfer.

raii_file_descriptor.cpp
#include <unistd.h>   // open, close, read, write
#include <fcntl.h>    // O_RDONLY, etc.
#include <stdexcept>
#include <utility>
#include <iostream>

class FileDescriptor {
    int fd_ = -1;  // -1 = no resource
public:
    // Acquire: open file in constructor
    explicit FileDescriptor(const char* path, int flags)
        : fd_(::open(path, flags)) {
        if (fd_ < 0) throw std::runtime_error("Cannot open file");
    }

    // Release: close file in destructor
    ~FileDescriptor() {
        if (fd_ >= 0) ::close(fd_);
    }

    // Non-copyable (copying a file descriptor is meaningless)
    FileDescriptor(const FileDescriptor&) = delete;
    FileDescriptor& operator=(const FileDescriptor&) = delete;

    // Moveable (transfer ownership)
    FileDescriptor(FileDescriptor&& other) noexcept
        : fd_(std::exchange(other.fd_, -1)) {}

    FileDescriptor& operator=(FileDescriptor&& other) noexcept {
        if (this != &other) {
            if (fd_ >= 0) ::close(fd_);
            fd_ = std::exchange(other.fd_, -1);
        }
        return *this;
    }

    int get() const { return fd_; }
};

int main() {
    try {
        FileDescriptor fd("/tmp/raii_test.txt", O_RDONLY);
        std::cout << "File opened: fd=" << fd.get() << '\n';
        // Use fd...
    } catch (const std::exception& e) {
        std::cerr << e.what() << '\n';
    }
    // fd.~FileDescriptor() called automatically — file closed
    // Even if an exception was thrown, the file is closed
}

Εξαίρεση Ασφάλεια μέσω RAII

Χωρίς RAII, ο κώδικας που είναι ασφαλής για εξαιρέσεις απαιτεί κουραστικά μπλοκ try/catch για κάθε πόρο. Με το RAII, είναι αυτόματο — κάθε καταστροφέας ενεργοποιείται κατά το ξετύλιγμα της στοίβας.

exception_safety.cpp
#include <memory>
#include <mutex>
#include <fstream>
#include <vector>
#include <stdexcept>

std::mutex g_mutex;

// WITHOUT RAII — manual cleanup is error-prone
void bad_example() {
    std::mutex* m = &g_mutex;
    m->lock();
    int* data = new int[100];
    // If this throws, mutex stays locked AND data leaks!
    // process(data);
    delete[] data;
    m->unlock();
}

// WITH RAII — automatic, exception-safe cleanup
void good_example() {
    std::lock_guard<std::mutex> lock(g_mutex);  // RAII lock
    auto data = std::make_unique<int[]>(100);   // RAII allocation
    // If this throws:
    //   1. data's destructor frees the memory
    //   2. lock's destructor unlocks the mutex
    //   Stack unwinding handles everything
    // process(data.get());
}

// Multiple resources — RAII composes naturally
void multi_resource() {
    std::lock_guard<std::mutex> lock(g_mutex);
    auto buffer = std::make_unique<char[]>(4096);
    std::ofstream file("/tmp/output.txt");

    // If any operation throws, ALL resources are released
    // in reverse order of construction:
    //   1. file closed
    //   2. buffer freed
    //   3. mutex unlocked
}

lock_guard, unique_lock & scoped_lock

Η τυπική βιβλιοθήκη παρέχει περιτυλίγματα RAII για mutexes:

- std::lock_guard — η απλούστερη κλειδαριά RAII. Κλειδώνει στον κατασκευαστή, ξεκλειδώνει στον καταστροφέα. Μη μετακινήσιμο, μη αντιγράψιμο. Χρησιμοποιήστε το όταν χρειάζεστε μια απλή κλειδαριά με εμβέλεια.

- std::unique_lock — εύκαμπτη κλειδαριά RAII. Υποστηρίζει καθυστερημένο κλείδωμα, χρονομετρημένο κλείδωμα, χειροκίνητο ξεκλείδωμα/επανακλείδωμα και μπορεί να μετακινηθεί. Απαιτείται για χρήση μεstd::condition_variable.

- std::scoped_lock (C++17) — κλειδώνει πολλαπλά mutexes ταυτόχρονα χρησιμοποιώντας έναν αλγόριθμο αποφυγής αδιεξόδου. Αυτός είναι ο προτιμώμενος τύπος κλειδώματος όταν κλειδώνετε ένα ή περισσότερα mutexe.

std::mutex m1, m2;
// C++17: lock both without deadlock risk
std::scoped_lock lock(m1, m2);
Παγίδα

1. Τα μη κατονομασμένα αντικείμενα RAII καταστρέφονται αμέσως:

std::lock_guard<std::mutex>(mtx); // BUG! Temporary — locks and immediately unlocks
std::lock_guard<std::mutex> lock(mtx); // Correct — named, lives until scope end

2. Never naively copy RAII types: Copying a file handle or mutex lock makes no sense. RAII types should be:
- Move-only (like unique_ptr, unique_lock) for exclusive ownership, or
- Reference-counted (like shared_ptr) for shared ownership.

3. Destructor should not throw: If a destructor throws during stack unwinding (another exception is active), std::terminate is called. Destructors are noexcept από προεπιλογή στο C++11+.

Βέλτιστη πρακτική

1. Κάθε πόρος πρέπει να ανήκει σε ένα αντικείμενο RAII — μνήμη, αρχεία, κλειδαριές, υποδοχές, συνδέσεις βάσης δεδομένων, λαβές GPU.
2. Ένας πόρος ανά κατηγορία RAII — κρατήστε τα περιτυλίγματα εστιασμένα.
3. Διαγραφή λειτουργιών αντιγραφής εκτός εάν η αντιγραφή του πόρου έχει νόημα.
4. Εφαρμόστε λειτουργίες μετακίνησης για να ενεργοποιήσετε τη μεταβίβαση ιδιοκτησίας.
5. Χρησιμοποιήστε τοstd::exchangeσε λειτουργίες μετακίνησης — διαβάζει ατομικά και επαναφέρει την πηγή:fd_ = std::exchange(other.fd_, -1);
6. Προτιμήστε τυπικούς τύπους RAII (unique_ptr,lock_guard,fstream) — γράψτε μόνο προσαρμοσμένα περιτυλίγματα για πόρους χωρίς τυπικά περιτυλίγματα.

Composing RAII Types (Rule of Zero)

Η τελική έκφραση του RAII είναι ο Κανόνας του Μηδέν: εάν όλες οι μεταβλητές μελών σας είναι τύπους RAII, η κλάση σας δεν χρειάζεται δεν έχει δηλωθεί από τον χρήστη ειδικά μέλη. Οι λειτουργίες καταστροφέα, αντιγραφής και μετακίνησης που δημιουργούνται από τον μεταγλωττιστή κάνουν το σωστό αυτόματα.

class DatabaseSession {
    std::unique_ptr<Connection> conn_;    // RAII: auto-closed
    std::unique_lock<std::mutex> lock_;   // RAII: auto-unlocked
    std::vector<std::string> log_;        // RAII: auto-freed
    // No destructor, copy, or move needed — all member types handle it
};

Ο κανόνας του μηδενός είναι ο στόχος. Ο κανόνας των πέντε (ορίστε ρητά και τα 5 ειδικά μέλη) είναι για τύπους περιτυλίγματος πόρων χαμηλού επιπέδου. Δεν υπάρχει μέση λύση — αν δηλώσετε κάποιο από τα πέντε, δηλώστε τα όλα.

Βασικά συμπεράσματα
  • RAII: απόκτηση στον κατασκευαστή, απελευθέρωση σε καταστροφέα — αυτόματη και ασφαλής για εξαιρέσεις
  • Οι καταστροφείς καλούνται κατά το ξετύλιγμα της στοίβας, επομένως το RAII χειρίζεται τις εξαιρέσεις φυσικά
  • Οι τύποι RAII θα πρέπει να είναι μόνο μετακίνησης (αποκλειστική ιδιοκτησία) ή με μέτρηση αναφοράς (κοινή ιδιοκτησία)
  • Χρησιμοποιήστε τοstd::scoped_lock(C++17) για κλείδωμα πολλαπλών mutex χωρίς αδιέξοδο
  • Τα μη κατονομασμένα αντικείμενα RAII καταστρέφονται αμέσως — δώστε τους πάντα ένα όνομα
  • Follow the Rule of Zero: use RAII members so the compiler generates correct special members

Κουίζ — Δοκιμάστε τις γνώσεις σας

(15 XP)

1. What is wrong with`std::lock_guard<std::mutex>(mtx);`(note: no variable name)?

2. Why should RAII types typically delete their copy operations?

3. Τι είναι ο κανόνας του μηδέν;