Co oznacza przejęcie zasobów to inicjalizacja (RAII)?


Odpowiedzi:


374

To naprawdę okropna nazwa niesamowicie potężnej koncepcji i być może jedna z najważniejszych rzeczy, za którymi twórcy C ++ tęsknią, gdy przechodzą na inne języki. Nastąpił pewien ruch, aby spróbować zmienić nazwę tej koncepcji na zarządzanie zasobami związane z zakresem , choć wydaje się, że nie została jeszcze przyjęta.

Kiedy mówimy „Zasób”, nie mamy na myśli tylko pamięci - mogą to być uchwyty plików, gniazda sieciowe, uchwyty baz danych, obiekty GDI ... Krótko mówiąc, rzeczy, których mamy skończoną podaż i dlatego musimy być w stanie kontrolować ich użycie. Aspekt „związany z zasięgiem” oznacza, że ​​czas życia obiektu jest związany z zakresem zmiennej, więc gdy zmienna wykracza poza zakres, destruktor zwolni zasób. Bardzo przydatną właściwością tego jest to, że zapewnia większe bezpieczeństwo wyjątków. Na przykład porównaj to:

RawResourceHandle* handle=createNewResource();
handle->performInvalidOperation();  // Oops, throws exception
...
deleteResource(handle); // oh dear, never gets called so the resource leaks

Z RAII

class ManagedResourceHandle {
public:
   ManagedResourceHandle(RawResourceHandle* rawHandle_) : rawHandle(rawHandle_) {};
   ~ManagedResourceHandle() {delete rawHandle; }
   ... // omitted operator*, etc
private:
   RawResourceHandle* rawHandle;
};

ManagedResourceHandle handle(createNewResource());
handle->performInvalidOperation();

W tym ostatnim przypadku, gdy wyjątek zostanie zgłoszony, a stos zostanie rozwinięty, zmienne lokalne zostaną zniszczone, co gwarantuje, że nasz zasób zostanie oczyszczony i nie wycieknie.


2
@ themandrill: Próbowałem ideone.com/1Jjzuc tego programu. Ale nie ma wywołania destruktora. Tomdalling.com/blog/software-design/... mówi, że C ++ gwarantuje, że wywoływany zostanie destruktor obiektów na stosie, nawet jeśli zostanie zgłoszony wyjątek. Dlaczego więc niszczyciel tutaj nie zadziałał? Czy moje zasoby wyciekły, czy też nigdy nie zostaną uwolnione lub zwolnione?
Destructor

8
Zgłaszany jest wyjątek, ale użytkownik go nie łapie, więc aplikacja kończy działanie. Jeśli zastosujesz
the_mandrill

2
Nie jestem pewien, czy Scope-Boundjest to najlepszy wybór tutaj, ponieważ specyfikatory klasy pamięci wraz z zakresem określają czas przechowywania jednostki. Zawężenie go do zakresu ograniczonego może być użytecznym uproszczeniem, jednak nie jest w 100% precyzyjne
SebNag

125

To jest programowy idiom, który w skrócie oznacza, że ​​ty

  • enkapsuluje zasób do klasy (której konstruktor zwykle - ale niekoniecznie ** - przejmuje zasób, a jego destruktor zawsze go zwalnia)
  • użyj zasobu poprzez lokalną instancję klasy *
  • zasób jest automatycznie zwalniany, gdy obiekt wykracza poza zasięg

Gwarantuje to, że cokolwiek się stanie, gdy zasób jest w użyciu, ostatecznie zostanie uwolniony (czy to z powodu normalnego powrotu, zniszczenia zawierającego obiektu, czy zgłoszonego wyjątku).

Jest to powszechnie stosowana dobra praktyka w C ++, ponieważ poza tym, że jest bezpiecznym sposobem radzenia sobie z zasobami, sprawia również, że kod jest znacznie czystszy, ponieważ nie trzeba mieszać kodu obsługi błędów z główną funkcjonalnością.

* Aktualizacja: „lokalny” może oznaczać lokalną zmienną lub niestatyczną zmienną składową klasy. W tym drugim przypadku zmienna składowa jest inicjowana i niszczona za pomocą obiektu właściciela.

** Update2: jak wskazał @sbi, zasób - choć często jest przydzielany w konstruktorze - może być również przydzielany na zewnątrz i przekazywany jako parametr.


1
AFAIK, akronim nie oznacza, że ​​obiekt musi znajdować się w zmiennej lokalnej (stosowej). Może to być zmienna składowa innego obiektu, więc gdy obiekt „trzymający” zostanie zniszczony, obiekt członkowski również zostanie zniszczony i zasób zostanie zwolniony. W rzeczywistości myślę, że akronim oznacza konkretnie tylko to, że nie ma open()/ close()metod inicjowania i zwalniania zasobu, tylko konstruktor i destruktor, więc „trzymanie” zasobu to tylko czas życia obiektu, bez względu na to, czy jest to czas życia obsługiwane przez kontekst (stos) lub jawnie (alokacja dynamiczna)
Javier

1
Właściwie nic nie mówi, że zasób musi zostać pozyskany w konstruktorze. Robią to strumienie plików, łańcuchy i inne kontenery, ale równie dobrze zasób może zostać przekazany do konstruktora, jak zwykle w przypadku inteligentnych wskaźników. Ponieważ twoja odpowiedź jest najbardziej pozytywna, możesz to naprawić.
sbi

To nie jest akronim, to skrót. IIRC większość ludzi wymawia to „ar ey ay ay”, więc tak naprawdę nie kwalifikuje się do akronimu takiego jak powiedz DARPA, który wymawia się DARPA zamiast ortograficznego. Powiedziałbym też, że RAII jest raczej paradygmatem niż zwykłym idiomem.
dtech,

@Peter Torok: Próbowałem tego programu ideone.com/1Jjzuc . Ale nie ma wywołania destruktora. Tomdalling.com/blog/software-design/... mówi, że gwarantuje, C ++, że destruktor obiektów na stosie zostanie wywołany, nawet jeśli jest wyjątek. Dlaczego więc niszczyciel tutaj nie zadziałał? Czy moje zasoby wyciekły, czy też nigdy nie zostaną uwolnione lub zwolnione?
Destructor

50

„RAII” oznacza „Pozyskiwanie zasobów to inicjalizacja” i jest w rzeczywistości dość mylące, ponieważ nie chodzi o pozyskiwanie zasobów (i inicjalizację obiektu), ale zwalnianie zasobu (poprzez zniszczenie obiektu ).
Ale RAII to nazwa, którą otrzymaliśmy i trzyma się.

W samym sercu tego idiomu znajduje się hermetyzacja zasobów (fragmenty pamięci, otwarte pliki, odblokowane muteksy, ty-nazwa-go) w lokalnych, automatycznych obiektach , a także destruktor tego obiektu zwalniający zasób, gdy obiekt jest niszczony na koniec zakresu, do którego należy:

{
  raii obj(acquire_resource());
  // ...
} // obj's dtor will call release_resource()

Oczywiście obiekty nie zawsze są lokalnymi, automatycznymi obiektami. Mogą też należeć do klasy:

class something {
private:
  raii obj_;  // will live and die with instances of the class
  // ... 
};

Jeśli takie obiekty zarządzają pamięcią, często nazywane są „inteligentnymi wskaźnikami”.

Istnieje wiele odmian tego. Na przykład w pierwszym fragmencie kodu pojawia się pytanie, co by się stało, gdyby ktoś chciał skopiować obj. Najłatwiejszym sposobem byłoby po prostu zabronienie kopiowania. std::unique_ptr<>, inteligentny wskaźnik, który ma być częścią standardowej biblioteki opisanej w następnym standardzie C ++, robi to.
Kolejny taki inteligentny wskaźnik std::shared_ptrzawiera „współwłasność” zasobu (dynamicznie alokowanego obiektu), który posiada. Oznacza to, że można go swobodnie kopiować, a wszystkie kopie odnoszą się do tego samego obiektu. Inteligentny wskaźnik śledzi, ile kopii odnosi się do tego samego obiektu i usuwa go, gdy ostatni zostanie zniszczony.
Trzeci wariant jest opisany przezstd::auto_ptr który implementuje pewien rodzaj semantyki ruchu: Obiekt jest własnością tylko jednego wskaźnika, a próba skopiowania obiektu spowoduje (poprzez hackery składniowe) przeniesienie własności obiektu na obiekt docelowy operacji kopiowania.


4
std::auto_ptrjest przestarzałą wersją std::unique_ptr. std::auto_ptrrodzaj symulowanej semantyki ruchów, o ile było to możliwe w C ++ 98, std::unique_ptrwykorzystuje nową semantykę ruchu C ++ 11. Nowa klasa została utworzona, ponieważ semantyka przenoszenia w C ++ 11 jest bardziej wyraźna (wymaga std::moveoprócz tymczasowego), podczas gdy była domyślna dla każdej kopii z non-const w std::auto_ptr.
Jan Hudec,

@JiahaoCai: Raz, wiele lat temu (w Usenecie), sam Stroustrup tak powiedział.
sbi

21

Czas życia obiektu zależy od jego zakresu. Czasami jednak potrzebujemy lub warto stworzyć obiekt, który żyje niezależnie od zakresu, w którym został utworzony. W C ++ operator newsłuży do utworzenia takiego obiektu. Aby zniszczyć obiekt, deletemożna użyć operatora . Obiekty tworzone przez operatora newsą alokowane dynamicznie, tj. Alokowane w pamięci dynamicznej (zwanej także stertą lub wolną pamięcią ). Tak więc obiekt, który został utworzony, newbędzie istniał, dopóki nie zostanie jawnie zniszczony przy użyciu delete.

Niektóre błędy, które mogą wystąpić podczas używania newi deleteto:

  • Wyciekły obiekt (lub pamięć): za pomocą, newaby przydzielić obiekt i zapomnieć o deletenim.
  • Przedwczesne usunięcie (lub zwisające odniesienie ): przytrzymanie innego wskaźnika na obiekcie, deleteobiekcie, a następnie użycie drugiego wskaźnika.
  • Podwójne usunięcie : dwukrotna próba deleteobiektu.

Zasadniczo preferowane są zmienne o zasięgu. Jednak RAII może być użyte jako alternatywa newi deleteaby obiekt działał niezależnie od jego zakresu. Taka technika polega na zabraniu wskaźnika do obiektu przydzielonego na stercie i umieszczeniu go w obiekcie uchwytu / menedżera . Ten ostatni ma destruktor, który zajmie się zniszczeniem obiektu. Zagwarantuje to, że obiekt będzie dostępny dla dowolnej funkcji, która chce uzyskać do niego dostęp, oraz że obiekt zostanie zniszczony po zakończeniu okresu użytkowania obiektu uchwytu , bez potrzeby jawnego czyszczenia.

Przykłady ze standardowej biblioteki C ++ używającej RAII to std::stringi std::vector.

Rozważ ten fragment kodu:

void fn(const std::string& str)
{
    std::vector<char> vec;
    for (auto c : str)
        vec.push_back(c);
    // do something
}

kiedy tworzysz wektor i wpychasz do niego elementy, nie przejmujesz się przydzielaniem i zwalnianiem takich elementów. Wektor używa newdo przydzielenia miejsca dla swoich elementów na stercie i deletedo zwolnienia tego miejsca. Jako użytkownik wektora nie przejmujesz się szczegółami implementacji i ufasz, że wektor nie wycieknie. W tym przypadku wektor jest obiektem uchwytu jego elementów.

Inne przykłady z biblioteki standardowej że stosowanie RAII są std::shared_ptr, std::unique_ptri std::lock_guard.

Inną nazwą tej techniki jest SBRM , skrót od Scope-Bound Resource Management .


1
„SBRM” ma dla mnie o wiele więcej sensu. Przyszedłem do tego pytania, ponieważ myślałem, że rozumiem RAII, ale nazwa mnie wytrąciła z równowagi, słysząc, że jest to opisywane jako „Zarządzanie zasobami ograniczonymi zasięgiem”, natychmiast uświadomiło mi, że rzeczywiście zrozumiałem tę koncepcję.
JShorthouse

Nie jestem pewien, dlaczego nie oznaczono tego jako odpowiedzi na pytanie. To bardzo dokładna i dobrze napisana odpowiedź, dzięki @elmiomar
Abdelrahman Shoman

13

Książka C ++ Programming with Design Patterns Revealed opisuje RAII jako:

  1. Pozyskiwanie wszystkich zasobów
  2. Korzystanie z zasobów
  3. Zwalnianie zasobów

Gdzie

  • Zasoby są implementowane jako klasy, a wszystkie wskaźniki mają wokół siebie opakowania klas (co czyni je inteligentnymi wskaźnikami).

  • Zasoby są nabywane przez inwokowanie ich konstruktorów i uwalniane pośrednio (w odwrotnej kolejności pozyskiwania) przez inwokację ich destruktorów.


1
@Brandin Zredagowałem swój post, aby czytelnicy skupili się na istotnych treściach, zamiast dyskutować o szarej strefie prawa autorskiego dotyczącego dozwolonego użytku.
Dennis

7

Klasa RAII składa się z trzech części:

  1. Zasób zostaje zlikwidowany w destruktorze
  2. Instancje klasy są przydzielane stosowo
  3. Zasób jest nabywany w konstruktorze. Ta część jest opcjonalna, ale powszechna.

RAII oznacza „Pozyskiwanie zasobów to inicjalizacja”. Część RAII dotycząca „pozyskiwania zasobów” polega na rozpoczęciu czegoś, co należy zakończyć później, na przykład:

  1. Otwieranie pliku
  2. Przydzielanie pamięci
  3. Nabycie zamka

Część „jest inicjalizacja” oznacza, że ​​akwizycja odbywa się wewnątrz konstruktora klasy.

https://www.tomdalling.com/blog/software-design/resource-acquisition-is-initialisation-raii-explained/


5

Ręczne zarządzanie pamięcią to koszmar, który programiści wymyślają sposoby, których można uniknąć od czasu wynalezienia kompilatora. Języki programowania za pomocą śmieciarek ułatwiają życie, ale kosztem wydajności. W tym artykule - Eliminowanie Garbage Collector: The RAII Way , inżynier z Toptal, Peter Goodspeed-Niklaus, zagląda do historii śmieciarek i wyjaśnia, w jaki sposób pojęcia własności i pożyczek mogą pomóc wyeliminować śmieciarzy bez narażania ich gwarancji bezpieczeństwa.

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.