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.
źródło
:(
Czytałem wtedy cały wątek i nawet nie uważałem się za nowicjusza C ++!Odpowiedzi:
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
FileHandle
klasy wykorzystującej RAII: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:
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:
Konstruktor programu
Logger
zawiedzie, jeślioriginal
konstruktor ulegnie awarii (ponieważfilename1
nie można go otworzyć),duplex
konstruktor ulegnie awarii (ponieważfilename2
nie można go otworzyć) lub zapis do plików wLogger
treści konstruktora nie powiedzie się. W żadnym z tych przypadkówLogger
destruktor nie zostanie wywołany - więc nie możemy na nim polegać,Logger
aby zwolnił pliki. Ale jeślioriginal
został skonstruowany, jego destruktor zostanie wywołany podczas czyszczeniaLogger
konstruktora.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ładnikiemLogger
, 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
IDisposable
po 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 .
źródło
Foo
jest jej właścicielemBar
iBoz
mutuje, ...Istnieją doskonałe odpowiedzi, więc po prostu dodaję zapomniane rzeczy.
0. RAII dotyczy zakresów
RAII dotyczy obu:
Inni już na to odpowiadali, więc nie będę się rozpisywał.
1. Kiedy kodujesz w Javie lub C #, już używasz RAII ...
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
synchronized
golock
):... już używa RAII: Pozyskiwanie mutexów odbywa się w słowie kluczowym (
synchronized
lublock
), 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
synchronized
anilock
w C ++, ale nadal możemy je mieć.W C ++ byłoby napisane:
który można łatwo napisać w sposób Java / C # (używając makr C ++):
2. RAII mają alternatywne zastosowania
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:
które oczywiście można napisać ponownie w Javie / C # za pomocą makra:
3. Dlaczego brakuje C ++
finally
?finally
Klauzula jest stosowana w C # / Java, aby obsłużyć utylizacji zasobów w przypadku wyprowadzenia zakres (za pomocąreturn
albo 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
finally
klauzula 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 ++
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 ++:
źródło
Proszę zobaczyć:
Czy programiści innych języków oprócz C ++ używają, znają lub rozumieją RAII?
RAII i inteligentne wskaźniki w C ++
Czy C ++ obsługuje bloki „w końcu”? (A co to za „RAII”, o którym ciągle słyszę?)
RAII a wyjątki
itp..
źródło
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:
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.
źródło
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
finally
klauzulach.źródło
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).
źródło
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.
źródło
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.
źródło
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ć
Nie, nie bardzo.
Nigdy. Wyrzucanie elementów bezużytecznych rozwiązuje tylko bardzo mały podzbiór dynamicznego zarządzania zasobami.
źródło
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.
źródło
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.
źródło