Co oznacza przejęcie zasobów to inicjalizacja (RAII)?
Co oznacza przejęcie zasobów to inicjalizacja (RAII)?
Odpowiedzi:
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.
Scope-Bound
jest 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
To jest programowy idiom, który w skrócie oznacza, że ty
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.
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)
„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_ptr
zawiera „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.
std::auto_ptr
jest przestarzałą wersją std::unique_ptr
. std::auto_ptr
rodzaj symulowanej semantyki ruchów, o ile było to możliwe w C ++ 98, std::unique_ptr
wykorzystuje nową semantykę ruchu C ++ 11. Nowa klasa została utworzona, ponieważ semantyka przenoszenia w C ++ 11 jest bardziej wyraźna (wymaga std::move
oprócz tymczasowego), podczas gdy była domyślna dla każdej kopii z non-const w std::auto_ptr
.
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 new
służy do utworzenia takiego obiektu. Aby zniszczyć obiekt, delete
można użyć operatora . Obiekty tworzone przez operatora new
są alokowane dynamicznie, tj. Alokowane w pamięci dynamicznej (zwanej także stertą lub wolną pamięcią ). Tak więc obiekt, który został utworzony, new
będzie istniał, dopóki nie zostanie jawnie zniszczony przy użyciu delete
.
Niektóre błędy, które mogą wystąpić podczas używania new
i delete
to:
new
aby przydzielić obiekt i zapomnieć o delete
nim.delete
obiekcie, a następnie użycie drugiego wskaźnika.delete
obiektu.Zasadniczo preferowane są zmienne o zasięgu. Jednak RAII może być użyte jako alternatywa new
i delete
aby 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::string
i 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 new
do przydzielenia miejsca dla swoich elementów na stercie i delete
do 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_ptr
i std::lock_guard
.
Inną nazwą tej techniki jest SBRM , skrót od Scope-Bound Resource Management .
Książka C ++ Programming with Design Patterns Revealed opisuje RAII jako:
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.
Klasa RAII składa się z trzech części:
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:
Część „jest inicjalizacja” oznacza, że akwizycja odbywa się wewnątrz konstruktora klasy.
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.