Zrozumienie znaczenia terminu i koncepcji - RAII (pozyskiwanie zasobów to inicjalizacja)


110

Czy moglibyście jako programiści C ++ dać nam dobry opis tego, czym jest RAII, dlaczego jest ważny i czy może mieć jakiekolwiek znaczenie dla innych języków?

I zrobić znać trochę. Uważam, że oznacza to „Pozyskiwanie zasobów to inicjalizacja”. Jednak ta nazwa nie współgra z moim (prawdopodobnie niepoprawnym) zrozumieniem, czym jest RAII: mam wrażenie, że RAII jest sposobem na inicjalizację obiektów na stosie w taki sposób, że kiedy te zmienne wyjdą poza zakres, destruktory automatycznie nazwać powodując oczyszczenie zasobów.

Dlaczego więc nie nazywa się to „użyciem stosu do wyzwalania czyszczenia” (UTSTTC :)? Jak dostać się stamtąd do „RAII”?

I jak możesz stworzyć na stosie coś, co spowoduje wyczyszczenie czegoś, co żyje na stercie? Czy są też przypadki, w których nie można używać RAII? Czy zdarzyło Ci się, że chciałeś zbierać śmieci? Przynajmniej garbage collector, którego można by użyć dla niektórych obiektów, a innymi pozwolić na zarządzanie?

Dzięki.


27
UTSTTC? Lubię to! Jest o wiele bardziej intuicyjny niż RAII. RAII jest źle nazwany, wątpię, czy jakikolwiek programista C ++ mógłby temu zaprzeczyć. Ale zmiana nie jest łatwa. ;)
jalf

10
Oto pogląd Stroustrupa w tej sprawie: groups.google.com/group/comp.lang.c++.moderated/msg/…
sbi

3
@sbi: W każdym razie +1 w komentarzu tylko do badań historycznych. Uważam, że posiadanie punktu widzenia autora (B. Stroustrup) na temat nazwy pojęcia (RAII) jest na tyle interesujące, że można uzyskać własną odpowiedź.
paercebal,

1
@paercebal: Badania historyczne? Teraz sprawiłeś, że poczułem się bardzo stary. :(Czytałem wtedy cały wątek i nawet nie uważałem się za nowicjusza C ++!
sbi

3
+1, właśnie miałem zadać to samo pytanie, cieszę się, że nie tylko ja rozumiem koncepcję, ale nie rozumiem nazwy. Wydaje się, że powinien był nazywać się RAOI - Pozyskiwanie zasobów przy inicjalizacji.
laurent

Odpowiedzi:


132

Dlaczego więc nie nazywa się to „użyciem stosu do wyzwalania czyszczenia” (UTSTTC :)?

RAII mówi ci, co masz zrobić: zdobądź zasoby w konstruktorze! Dodałbym: jeden zasób, jeden konstruktor. UTSTTC to tylko jedno zastosowanie tego, RAII to znacznie więcej.

Zarządzanie zasobami jest do bani. Tutaj zasób to wszystko, co wymaga oczyszczenia po użyciu. Badania projektów na wielu platformach pokazują, że większość błędów jest związana z zarządzaniem zasobami - i jest to szczególnie złe w systemie Windows (ze względu na wiele typów obiektów i alokatorów).

W C ++ zarządzanie zasobami jest szczególnie skomplikowane ze względu na kombinację wyjątków i szablonów (w stylu C ++). Aby zajrzeć pod maskę, zobacz GOTW8 ).


C ++ gwarantuje, że destruktor jest wywoływany wtedy i tylko wtedy, gdy gdy konstruktor się powiódł. Opierając się na tym, RAII może rozwiązać wiele nieprzyjemnych problemów, o których przeciętny programista może nawet nie być świadomy. Oto kilka przykładów wykraczających poza „moje zmienne lokalne zostaną zniszczone za każdym razem, gdy wrócę”.

Zacznijmy od zbyt uproszczonej FileHandleklasy wykorzystującej RAII:

class FileHandle
{
    FILE* file;

public:

    explicit FileHandle(const char* name)
    {
        file = fopen(name);
        if (!file)
        {
            throw "MAYDAY! MAYDAY";
        }
    }

    ~FileHandle()
    {
        // The only reason we are checking the file pointer for validity
        // is because it might have been moved (see below).
        // It is NOT needed to check against a failed constructor,
        // because the destructor is NEVER executed when the constructor fails!
        if (file)
        {
            fclose(file);
        }
    }

    // The following technicalities can be skipped on the first read.
    // They are not crucial to understanding the basic idea of RAII.
    // However, if you plan to implement your own RAII classes,
    // it is absolutely essential that you read on :)



    // It does not make sense to copy a file handle,
    // hence we disallow the otherwise implicitly generated copy operations.

    FileHandle(const FileHandle&) = delete;
    FileHandle& operator=(const FileHandle&) = delete;



    // The following operations enable transfer of ownership
    // and require compiler support for rvalue references, a C++0x feature.
    // Essentially, a resource is "moved" from one object to another.

    FileHandle(FileHandle&& that)
    {
        file = that.file;
        that.file = 0;
    }

    FileHandle& operator=(FileHandle&& that)
    {
        file = that.file;
        that.file = 0;
        return *this;
    }
}

Jeśli konstrukcja nie powiedzie się (z wyjątkiem), żadna inna funkcja składowa - nawet destruktor - nie zostanie wywołana.

RAII unika używania obiektów w nieprawidłowym stanie. to już ułatwia życie, zanim jeszcze użyjemy obiektu.

Przyjrzyjmy się teraz tymczasowym obiektom:

void CopyFileData(FileHandle source, FileHandle dest);

void Foo()
{
    CopyFileData(FileHandle("C:\\source"), FileHandle("C:\\dest"));
}

Istnieją trzy przypadki błędów do rozwiązania: nie można otworzyć pliku, można otworzyć tylko jeden plik, można otworzyć oba pliki, ale kopiowanie plików nie powiodło się. W implementacji innej niż RAII,Foo musiałby jawnie obsłużyć wszystkie trzy przypadki.

RAII zwalnia zasoby, które zostały nabyte, nawet jeśli w ramach jednej instrukcji pozyskano wiele zasobów.

Teraz zgromadźmy kilka obiektów:

class Logger
{
    FileHandle original, duplex;   // this logger can write to two files at once!

public:

    Logger(const char* filename1, const char* filename2)
    : original(filename1), duplex(filename2)
    {
        if (!filewrite_duplex(original, duplex, "New Session"))
            throw "Ugh damn!";
    }
}

Konstruktor programu Loggerzawiedzie, jeśli originalkonstruktor ulegnie awarii (ponieważ filename1nie można go otworzyć), duplexkonstruktor ulegnie awarii (ponieważ filename2nie można go otworzyć) lub zapis do plików w Loggertreści konstruktora nie powiedzie się. W żadnym z tych przypadków Loggerdestruktor nie zostanie wywołany - więc nie możemy na nim polegać, Loggeraby zwolnił pliki. Ale jeśli originalzostał skonstruowany, jego destruktor zostanie wywołany podczas czyszczenia Loggerkonstruktora.

RAII upraszcza czyszczenie po częściowej konstrukcji.


Punkty ujemne:

Punkty ujemne? Wszystkie problemy można rozwiązać za pomocą RAII i inteligentnych wskaźników ;-)

RAII jest czasami nieporęczny, gdy trzeba opóźnić akwizycję, wypychając zagregowane obiekty na stertę.
Wyobraź sobie, że Logger potrzebuje pliku SetTargetFile(const char* target). W takim przypadku uchwyt, który nadal musi być składnikiem Logger, musi znajdować się na stercie (np. W inteligentnym wskaźniku, aby odpowiednio wywołać zniszczenie uchwytu).

Tak naprawdę nigdy nie marzyłem o zbieraniu śmieci. Kiedy robię C #, czasami czuję chwilę błogości, której po prostu nie muszę się przejmować, ale o wiele bardziej brakuje mi wszystkich fajnych zabawek, które można stworzyć poprzez deterministyczną destrukcję. (używanie IDisposablepo prostu go nie tnie.)

Miałem jedną szczególnie złożoną strukturę, która mogłaby skorzystać na GC, gdzie „proste” inteligentne wskaźniki powodowałyby cykliczne odwołania w wielu klasach. Poradziliśmy sobie, ostrożnie równoważąc mocne i słabe wskazówki, ale za każdym razem, gdy chcemy coś zmienić, musimy przestudiować duży wykres relacji. GC mogło być lepsze, ale niektóre składniki zawierały zasoby, które powinny zostać wydane jak najszybciej.


Uwaga dotycząca próbki FileHandle: nie miała być kompletna, tylko próbka - ale okazała się niepoprawna. Dziękuję Johannesowi Schaubowi za wskazanie i FredOverflow za przekształcenie go w poprawne rozwiązanie C ++ 0x. Z czasem zdecydowałem się na podejście udokumentowane tutaj .


1
+1 Za wskazanie, że GC i ASAP nie zazębiają się. Nie boli często, ale kiedy już nie jest łatwo zdiagnozować: /
Matthieu M.

10
Szczególnie jedno zdanie, które przeoczyłem we wcześniejszych czytaniach. Powiedziałeś, że „RAII” mówi ci: „Zdobądź swoje zasoby wewnątrz konstruktorów”. To ma sens i jest prawie dosłowną parafrazą słowa „RAII”. Teraz rozumiem to jeszcze lepiej (zagłosowałbym ponownie, gdybym mógł :)
Charlie Flowers

2
Jedną z głównych zalet GC jest to, że struktura alokacji pamięci może zapobiegać tworzeniu wiszących odwołań w przypadku braku „niebezpiecznego” kodu (oczywiście jeśli „niebezpieczny” kod jest dozwolony, struktura nie może niczego zapobiec). GC jest również często lepszy od RAII, gdy ma do czynienia ze współużytkowanymi niezmiennymi obiektami, takimi jak łańcuchy, które często nie mają wyraźnego właściciela i nie wymagają czyszczenia. Szkoda, że ​​więcej frameworków nie stara się łączyć GC i RAII, ponieważ większość aplikacji będzie miała mieszankę niezmiennych obiektów (gdzie GC byłby najlepszy) i obiektów, które wymagają czyszczenia (gdzie RAII jest najlepszy).
supercat

@supercat: Generalnie lubię GC - ale działa tylko w przypadku zasobów, które GC „rozumie”. Np. .NET GC nie zna kosztu obiektów COM. Kiedy po prostu tworzysz i niszczysz je w pętli, z radością pozwoli aplikacji wejść na ziemię w zakresie przestrzeni adresowej lub pamięci wirtualnej - cokolwiek będzie pierwsze - nawet nie myśląc o zrobieniu GC. --- ponadto, nawet w środowisku doskonale wykonanym metodą GC, wciąż brakuje mi siły deterministycznej destrukcji: możesz zastosować ten sam wzór do innych artefaktów, np. pokazujących elementy UI w określonych warunkach.
peterchen

@peterchen: Myślę, że w wielu myślach związanych z OOP nie ma jednej rzeczy, a jest to koncepcja własności obiektu. Śledzenie własności jest często wyraźnie konieczne w przypadku obiektów z zasobami, ale często jest również konieczne w przypadku obiektów modyfikowalnych bez zasobów. Ogólnie rzecz biorąc, obiekty powinny hermetyzować swój zmienny stan w odwołaniach do prawdopodobnie współużytkowanych niezmiennych obiektów lub w obiektach mutowalnych, których są wyłącznym właścicielem. Taka wyłączna własność niekoniecznie oznacza wyłączny dostęp do zapisu, ale jeśli Foojest jej właścicielem Bari Bozmutuje, ...
supercat

42

Istnieją doskonałe odpowiedzi, więc po prostu dodaję zapomniane rzeczy.

0. RAII dotyczy zakresów

RAII dotyczy obu:

  1. pozyskiwanie zasobu (bez względu na to, jaki zasób) w konstruktorze i cofanie jego nabycia w destruktorze.
  2. wykonanie konstruktora, gdy zmienna jest zadeklarowana, i wykonanie destruktora automatycznie, gdy zmienna wyjdzie poza zakres.

Inni już na to odpowiadali, więc nie będę się rozpisywał.

1. Kiedy kodujesz w Javie lub C #, już używasz RAII ...

MONSIEUR JOURDAIN: Co! Kiedy mówię: „Nicole, przynieś mi moje kapcie i daj mi szlafmycę”, to proza?

MISTRZ FILOZOFII: Tak, sir.

MONSIEUR JOURDAIN: Od ponad czterdziestu lat mówię prozą, nic o tym nie wiedząc, i jestem ci bardzo zobowiązany, że mnie tego nauczyłeś.

- Molière: Dżentelmen klasy średniej, Akt 2, Scena 4

Podobnie jak Monsieur Jourdain z prozą, ludzie C #, a nawet Java już używają RAII, ale w ukryty sposób. Na przykład następujący kod Java (który jest napisany w ten sam sposób w C #, zastępując synchronizedgo lock):

void foo()
{
   // etc.

   synchronized(someObject)
   {
      // if something throws here, the lock on someObject will
      // be unlocked
   }

   // etc.
}

... już używa RAII: Pozyskiwanie mutexów odbywa się w słowie kluczowym ( synchronizedlub lock), a cofnięcie przejęcia zostanie wykonane przy opuszczaniu zakresu.

Jest to tak naturalne w swoim zapisie, że nie wymaga prawie żadnego wyjaśnienia nawet dla ludzi, którzy nigdy nie słyszeli o RAII.

Przewaga C ++ nad Javą i C # polega na tym, że za pomocą RAII można zrobić wszystko. Na przykład nie ma bezpośredniego wbudowanego odpowiednika synchronizedani lockw C ++, ale nadal możemy je mieć.

W C ++ byłoby napisane:

void foo()
{
   // etc.

   {
      Lock lock(someObject) ; // lock is an object of type Lock whose
                              // constructor acquires a mutex on
                              // someObject and whose destructor will
                              // un-acquire it 

      // if something throws here, the lock on someObject will
      // be unlocked
   }

   // etc.
}

który można łatwo napisać w sposób Java / C # (używając makr C ++):

void foo()
{
   // etc.

   LOCK(someObject)
   {
      // if something throws here, the lock on someObject will
      // be unlocked
   }

   // etc.
}

2. RAII mają alternatywne zastosowania

WHITE RABBIT: [śpiewa] Spóźniam się / Spóźniam się / Na bardzo ważną randkę. / Nie ma czasu, by powiedzieć „cześć”. / Do widzenia. / Jestem spóźniony, spóźniony, spóźniony.

- Alicja w Krainie Czarów (wersja Disneya, 1951)

Wiesz, kiedy konstruktor zostanie wywołany (przy deklaracji obiektu) i wiesz, kiedy zostanie wywołany odpowiadający mu destruktor (na wyjściu z zasięgu), więc możesz napisać prawie magiczny kod za pomocą tylko jednej linii. Witamy w krainie czarów C ++ (przynajmniej z punktu widzenia programisty C ++).

Na przykład, możesz napisać obiekt licznika (pozwolę sobie na to jako ćwiczenie) i używać go po prostu deklarując jego zmienną, tak jak został użyty obiekt lock powyżej:

void foo()
{
   double timeElapsed = 0 ;

   {
      Counter counter(timeElapsed) ;
      // do something lengthy
   }
   // now, the timeElapsed variable contain the time elapsed
   // from the Counter's declaration till the scope exit
}

które oczywiście można napisać ponownie w Javie / C # za pomocą makra:

void foo()
{
   double timeElapsed = 0 ;

   COUNTER(timeElapsed)
   {
      // do something lengthy
   }
   // now, the timeElapsed variable contain the time elapsed
   // from the Counter's declaration till the scope exit
}

3. Dlaczego brakuje C ++ finally?

[KRZYCZENIE] To ostatnie odliczanie!

- Europa: The Final Countdown (przepraszam, skończyły mi się cytaty, tutaj ... :-)

finallyKlauzula jest stosowana w C # / Java, aby obsłużyć utylizacji zasobów w przypadku wyprowadzenia zakres (za pomocą returnalbo rzucony wyjątek).

Wnikliwi czytelnicy specyfikacji zauważą, że C ++ nie ma klauzuli „last”. I to nie jest błąd, ponieważ C ++ tego nie potrzebuje, ponieważ RAII już zajmuje się usuwaniem zasobów. (I uwierz mi, napisanie destruktora w C ++ jest o wiele łatwiejsze niż pisanie odpowiedniej klauzuli Javy w końcu, czy nawet poprawnej metody Dispose w C #).

Nadal Zdarza się, że finallyklauzula będzie cool. Czy możemy to zrobić w C ++? Tak możemy! I znowu z alternatywnym użyciem RAII.

Wniosek: RAII to coś więcej niż filozofia w C ++: to C ++

RAII? TO JEST C ++ !!!

- oburzony komentarz programisty C ++, bezwstydnie skopiowany przez nieznanego króla Sparty i jego 300 przyjaciół

Kiedy osiągniesz pewien poziom doświadczenia w C ++, zaczynasz myśleć w kategoriach RAII , w kategoriach automatycznego wykonywania konstruktorów i destruktorów .

Zaczynasz myśleć w kategoriach zakresów , a znaki {i }stają się jednymi z najważniejszych w Twoim kodzie.

I prawie wszystko pasuje do RAII: bezpieczeństwo wyjątków, muteksy, połączenia z bazą danych, żądania do bazy danych, połączenie z serwerem, zegary, uchwyty systemu operacyjnego itp., I wreszcie pamięć.

Część bazy danych nie jest bez znaczenia, ponieważ jeśli zgadzasz się zapłacić cenę, możesz nawet pisać w stylu " programowania transakcyjnego ", wykonując wiersze i wiersze kodu, aż ostatecznie zdecydujesz, czy chcesz zatwierdzić wszystkie zmiany lub, jeśli nie jest to możliwe, cofnięcie wszystkich zmian (o ile każda linia spełnia co najmniej gwarancję silnego wyjątku). (zobacz drugą część artykułu Herb's Sutter o programowaniu transakcyjnym).

I jak puzzle, wszystko pasuje.

RAII jest tak dużą częścią C ++, że C ++ nie mógłby być C ++ bez niego.

To wyjaśnia, dlaczego doświadczeni programiści C ++ są tak zakochani w RAII i dlaczego RAII jest pierwszą rzeczą, której szukają, próbując innego języka.

I wyjaśnia, dlaczego Garbage Collector, będąc wspaniałą technologią samą w sobie, nie jest tak imponujący z punktu widzenia programisty C ++:

  • RAII już obsługuje większość spraw obsługiwanych przez GC
  • GC radzi sobie lepiej niż RAII z ​​cyklicznymi odwołaniami do czystych zarządzanych obiektów (złagodzone przez inteligentne wykorzystanie słabych wskaźników)
  • Wciąż GC jest ograniczone do pamięci, podczas gdy RAII może obsługiwać dowolny rodzaj zasobów.
  • Jak opisano powyżej, RAII może zrobić dużo, dużo więcej ...

Fan Javy: powiedziałbym, że GC jest o wiele bardziej użyteczny niż RAII, ponieważ obsługuje całą pamięć i uwalnia od wielu potencjalnych błędów. Dzięki GC możesz tworzyć odwołania cykliczne, zwracać i przechowywać referencje i trudno jest to zrobić źle (przechowywanie odwołania do rzekomo krótkotrwałego obiektu wydłuża jego czas życia, co jest rodzajem wycieku pamięci, ale to jedyny problem) . Obsługa zasobów za pomocą GC nie działa, ale większość zasobów w aplikacji ma trywialny cykl na żywo, a kilka pozostałych to nic wielkiego. Chciałbym mieć zarówno GC, jak i RAII, ale wydaje się to niemożliwe.
maaartinus

16

1
Niektóre z nich są zgodne z moim pytaniem, ale wyszukiwanie ich nie przyniosło, podobnie jak lista „pytań pokrewnych”, która pojawia się po wprowadzeniu nowego pytania. Dzięki za linki.
Charlie Flowers

1
@Charlie: Kompilacja w wyszukiwarce jest pod pewnymi względami bardzo słaba. Używanie składni tagu („[temat]”) jest bardzo pomocne, a wiele osób używa google ...
dmckee --- ex-moderator kitten

10

RAII używa semantyki destruktorów C ++ do zarządzania zasobami. Na przykład rozważmy inteligentny wskaźnik. Masz sparametryzowany konstruktor wskaźnika, który inicjuje ten wskaźnik z adresem obiektu. Przydzielasz wskaźnik na stosie:

SmartPointer pointer( new ObjectClass() );

Gdy inteligentny wskaźnik wychodzi poza zakres, destruktor klasy wskaźnika usuwa połączony obiekt. Wskaźnik jest przydzielany na stosie, a obiekt na stosie.

Są pewne przypadki, w których RAII nie pomaga. Na przykład, jeśli używasz inteligentnych wskaźników liczących referencje (takich jak boost :: shared_ptr) i utworzysz strukturę podobną do wykresu z cyklem, ryzykujesz przeciek pamięci, ponieważ obiekty w cyklu zapobiegną wzajemnemu uwalnianiu. Wyrzucanie śmieci mogłoby temu zapobiec.


2
Powinien więc nazywać się UCDSTMR :)
Daniel Daranas

Po zastanowieniu myślę, że UDSTMR jest bardziej odpowiedni. Podany jest język (C ++), więc litera „C” nie jest potrzebna w akronimie. UDSTMR oznacza używanie semantyki destruktora do zarządzania zasobami.
Daniel Daranas,

9

Chciałbym ująć to nieco mocniej niż poprzednie odpowiedzi.

RAII, Resource Acquisition Is Inicjalizacja oznacza, że ​​wszystkie pozyskane zasoby powinny zostać pozyskane w kontekście inicjalizacji obiektu. To zabrania "nagiego" pozyskiwania zasobów. Powodem jest to, że czyszczenie w C ++ działa na podstawie obiektu, a nie wywołania funkcji. Dlatego wszystkie czynności porządkowe powinny być wykonywane przez obiekty, a nie wywołania funkcji. W tym sensie C ++ jest bardziej zorientowany obiektowo niż np. Java. Oczyszczanie Java opiera się na wywołaniach funkcji w finallyklauzulach.


Świetna odpowiedź. A „inicjalizacja obiektu” oznacza „konstruktorów”, tak?
Charlie Flowers

@Charlie: tak, szczególnie w tym przypadku.
MSalters

8

Zgadzam się z zapaleniem mózgu. Chciałbym jednak dodać, że zasobami może być wszystko, nie tylko pamięć. Zasobem może być plik, sekcja krytyczna, wątek lub połączenie z bazą danych.

Nazywa się to pozyskiwaniem zasobów jest inicjalizacją, ponieważ zasób jest pozyskiwany, gdy konstruowany jest obiekt kontrolujący zasób. Jeśli konstruktor zawiódł (tj. Z powodu wyjątku), zasób nie zostanie pozyskany. Następnie, gdy obiekt znajdzie się poza zakresem, zasób jest zwalniany. c ++ gwarantuje, że wszystkie obiekty na stosie, które zostały pomyślnie skonstruowane, zostaną zniszczone (dotyczy to konstruktorów klas bazowych i składowych, nawet jeśli konstruktor superklasy zawiedzie).

Racjonalnym uzasadnieniem RAII jest zapewnienie bezpieczeństwa wyjątku pozyskiwania zasobów. Wszystkie pozyskane zasoby są prawidłowo zwalniane bez względu na miejsce wystąpienia wyjątku. Jednak zależy to od jakości klasy, która pozyskuje zasób (to musi być bezpieczne i jest to trudne).


Doskonale, dziękuję za wyjaśnienie powodów stojących za nazwą. Jak rozumiem, możesz sparafrazować RAII jako: „Nigdy nie zdobywaj żadnego zasobu poprzez inny mechanizm niż inicjalizacja (oparta na konstruktorze)”. Tak?
Charlie Flowers

Tak, to moja polityka, jednak bardzo uważam na pisanie własnych klas RAII, ponieważ muszą one być bezpieczne. Kiedy je piszę, staram się zapewnić bezpieczeństwo wyjątków poprzez ponowne wykorzystanie innych klas RAII napisanych przez ekspertów.
iain

Nie uważałem ich za trudne do napisania. Jeśli twoje zajęcia są wystarczająco małe, wcale nie są trudne.
Rob K

7

Problem z odśmiecaniem polega na tym, że tracisz deterministyczne zniszczenie, które jest kluczowe dla RAII. Gdy zmienna wyjdzie poza zakres, od modułu wyrzucania elementów bezużytecznych zależy, kiedy obiekt zostanie odzyskany. Zasób przechowywany przez obiekt będzie nadal przechowywany do momentu wywołania destruktora.


4
Problemem jest nie tylko determinizm. Prawdziwym problemem jest to, że finalizatory (nazewnictwo java) przeszkadzają GC. GC jest wydajny, ponieważ nie przywołuje martwych obiektów, a raczej ignoruje je w zapomnienie. GC muszą śledzić obiekty z finalizatorami w inny sposób, aby zagwarantować, że są nazywane
David Rodríguez - dribeas

1
z wyjątkiem java / c # prawdopodobnie wyczyściłbyś w bloku final, a nie w finalizatorze.
jk.

4

RAII pochodzi z Resource Allocation Is Initialization. Zasadniczo oznacza to, że gdy konstruktor zakończy wykonywanie, skonstruowany obiekt jest w pełni zainicjalizowany i gotowy do użycia. Oznacza to również, że destruktor zwolni wszelkie zasoby (np. Pamięć, zasoby systemu operacyjnego) należące do obiektu.

W porównaniu z językami / technologiami zbierania śmieci (np. Java, .NET), C ++ pozwala na pełną kontrolę życia obiektu. W przypadku obiektu przydzielonego na stosie będziesz wiedział, kiedy zostanie wywołany destruktor obiektu (kiedy wykonanie wyjdzie poza zakres), co nie jest tak naprawdę kontrolowane w przypadku czyszczenia pamięci. Nawet używając inteligentnych wskaźników w C ++ (np. Boost :: shared_ptr), będziesz wiedzieć, że gdy nie ma odniesienia do wskazanego obiektu, zostanie wywołany destruktor tego obiektu.


3

I jak możesz stworzyć na stosie coś, co spowoduje wyczyszczenie czegoś, co żyje na stercie?

class int_buffer
{
   size_t m_size;
   int *  m_buf;

   public:
   int_buffer( size_t size )
     : m_size( size ), m_buf( 0 )
   {
       if( m_size > 0 )
           m_buf = new int[m_size]; // will throw on failure by default
   }
   ~int_buffer()
   {
       delete[] m_buf;
   }
   /* ...rest of class implementation...*/

};


void foo() 
{
    int_buffer ib(20); // creates a buffer of 20 bytes
    std::cout << ib.size() << std::endl;
} // here the destructor is called automatically even if an exception is thrown and the memory ib held is freed.

Kiedy instancja int_buffer powstaje, musi mieć rozmiar i przydzieli niezbędną pamięć. Kiedy wychodzi poza zakres, wywoływany jest destruktor. Jest to bardzo przydatne w przypadku obiektów takich jak synchronizacja obiektów. Rozważać

class mutex
{
   // ...
   take();
   release();

   class mutex::sentry
   {
      mutex & mm;
      public:
      sentry( mutex & m ) : mm(m) 
      {
          mm.take();
      }
      ~sentry()
      {
          mm.release();
      }
   }; // mutex::sentry;
};
mutex m;

int getSomeValue()
{
    mutex::sentry ms( m ); // blocks here until the mutex is taken
    return 0;  
} // the mutex is released in the destructor call here.

Czy są też przypadki, w których nie można używać RAII?

Nie, nie bardzo.

Czy zdarzyło Ci się kiedyś mieć ochotę na zbieranie śmieci? Przynajmniej garbage collector, którego można by użyć dla niektórych obiektów, a innymi pozwolić na zarządzanie?

Nigdy. Wyrzucanie elementów bezużytecznych rozwiązuje tylko bardzo mały podzbiór dynamicznego zarządzania zasobami.


Używałem Javy i C # bardzo rzadko, więc nigdy nie mogłem tego przegapić, ale GC z pewnością utrudniało mi zarządzanie zasobami, kiedy musiałem ich używać, ponieważ nie mogłem używać RAII.
Rob K

1
Często używałem C # i zgadzam się z tobą w 100%. W rzeczywistości uważam niedeterministyczną GC za zobowiązanie w języku.
Nemanja Trifunovic

2

Jest tu już wiele dobrych odpowiedzi, ale chciałbym tylko dodać:
Prostym wyjaśnieniem RAII jest to, że w C ++ obiekt przydzielony na stosie jest niszczony za każdym razem, gdy wychodzi poza zakres. Oznacza to, że zostanie wywołany destruktor obiektów, który może wykonać wszystkie niezbędne czynności porządkowe.
Oznacza to, że jeśli obiekt jest tworzony bez "nowego", nie jest wymagane żadne "usuwanie". Taka jest też idea „inteligentnych wskaźników” - znajdują się one na stosie i zasadniczo zawijają obiekt oparty na stercie.


1
Nie, nie robią. Ale czy masz dobry powód, aby kiedykolwiek stworzyć inteligentny wskaźnik na stercie? Nawiasem mówiąc, inteligentny wskaźnik był tylko przykładem tego, gdzie RAII może być przydatna.
E Dominique

1
Może moje użycie "stosu" vs "sterty" jest trochę niechlujne - przez obiekt na "stosie" miałem na myśli dowolny obiekt lokalny. W naturalny sposób może być częścią obiektu np. Na stercie. Przez „utwórz inteligentny wskaźnik na stercie” miałem na myśli użycie new / delete na samym inteligentnym wskaźniku.
E Dominique

1

RAII to akronim od „Pozyskiwanie zasobów to inicjalizacja”.

Ta technika jest bardzo unikalna dla C ++ ze względu na ich obsługę zarówno konstruktorów, jak i niszczarek i prawie automatycznie konstruktory pasujące do przekazywanych argumentów lub w najgorszym przypadku domyślny konstruktor jest nazywany & destruktorami, jeśli podano jawność, w przeciwnym razie domyślny który jest dodawany przez kompilator C ++ jest wywoływany, jeśli nie napisałeś destruktora jawnie dla klasy C ++. Dzieje się tak tylko w przypadku obiektów C ++, które są automatycznie zarządzane - co oznacza, że ​​nie używają wolnego magazynu (pamięć przydzielona / cofnięta przy użyciu operatorów new, new [] / delete, delete [] C ++).

Technika RAII wykorzystuje tę automatycznie zarządzaną funkcję obiektu do obsługi obiektów, które są tworzone na stercie / wolnym magazynie, bezpośrednio prosząc o więcej pamięci przy użyciu nowego / nowego [], które powinno zostać jawnie zniszczone przez wywołanie funkcji delete / delete [] . Klasa obiektu zarządzanego automatycznie otoczy ten inny obiekt, który jest tworzony w pamięci stosu / wolnego magazynu. W związku z tym, gdy uruchamiany jest konstruktor obiektu zarządzanego automatycznie, opakowany obiekt jest tworzony w pamięci sterty / wolnego magazynu, a gdy uchwyt obiektu zarządzanego automatycznie wychodzi poza zakres, automatycznie wywoływany jest destruktor tego obiektu zarządzanego automatycznie, w którym obiekt jest niszczony za pomocą usuwania. Z koncepcjami OOP, jeśli umieścisz takie obiekty wewnątrz innej klasy w zakresie prywatnym, nie będziesz miał dostępu do opakowanych elementów członkowskich i metod & to jest powód, dla którego zaprojektowano inteligentne wskaźniki (aka klasy uchwytów). Te inteligentne wskaźniki ujawniają opakowany obiekt jako obiekt wpisany w typie zewnętrznym światu i tam, umożliwiając wywoływanie dowolnych elementów członkowskich / metod, z których składa się ujawniony obiekt pamięci. Zwróć uwagę, że inteligentne wskaźniki mają różne smaki w zależności od różnych potrzeb. Aby dowiedzieć się więcej na ten temat, zapoznaj się z dokumentacją dotyczącą programowania w nowoczesnym języku C ++ autorstwa Andrei Alexandrescu lub z implementacją / dokumentacją biblioteki boost (www.boostorg) shared_ptr.hpp. Mam nadzieję, że to pomoże ci zrozumieć RAII. Aby dowiedzieć się więcej na ten temat, zapoznaj się z dokumentacją dotyczącą programowania w nowoczesnym języku C ++ autorstwa Andrei Alexandrescu lub z implementacją / dokumentacją biblioteki boost (www.boostorg) shared_ptr.hpp. Mam nadzieję, że to pomoże ci zrozumieć RAII. Aby dowiedzieć się więcej na ten temat, zapoznaj się z dokumentacją dotyczącą programowania w nowoczesnym języku C ++ autorstwa Andrei Alexandrescu lub z implementacją / dokumentacją biblioteki boost (www.boostorg) shared_ptr.hpp. Mam nadzieję, że to pomoże ci zrozumieć RAII.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.