W praktyce z C ++, czym jest RAII , czym są inteligentne wskaźniki , jak są one implementowane w programie i jakie są zalety korzystania z RAII z inteligentnymi wskaźnikami?
źródło
W praktyce z C ++, czym jest RAII , czym są inteligentne wskaźniki , jak są one implementowane w programie i jakie są zalety korzystania z RAII z inteligentnymi wskaźnikami?
Prostym (i być może nadużywanym) przykładem RAII jest klasa File. Bez RAII kod może wyglądać mniej więcej tak:
File file("/path/to/file");
// Do stuff with file
file.close();
Innymi słowy, musimy upewnić się, że zamkniemy plik po zakończeniu. Ma to dwie wady - po pierwsze, gdziekolwiek używamy File, będziemy musieli wywołać File :: close () - jeśli zapomnimy to zrobić, będziemy trzymać plik dłużej niż jest to konieczne. Drugi problem dotyczy sytuacji, w której zgłoszony zostanie wyjątek przed zamknięciem pliku?
Java rozwiązuje drugi problem za pomocą klauzuli „wreszcie”:
try {
File file = new File("/path/to/file");
// Do stuff with file
} finally {
file.close();
}
lub od wersji Java 7 instrukcja try-with-resource:
try (File file = new File("/path/to/file")) {
// Do stuff with file
}
C ++ rozwiązuje oba problemy za pomocą RAII - to znaczy zamykając plik w destruktorze File. Tak długo, jak obiekt File zostanie zniszczony we właściwym czasie (którym i tak powinien być), zamknięcie pliku jest załatwione za nas. Nasz kod wygląda teraz tak:
File file("/path/to/file");
// Do stuff with file
// No need to close it - destructor will do that for us
Nie można tego zrobić w Javie, ponieważ nie ma gwarancji, że obiekt zostanie zniszczony, więc nie możemy zagwarantować, kiedy zasób taki jak plik zostanie zwolniony.
Na inteligentne wskaźniki - często tworzymy obiekty na stosie. Na przykład (i kradnąc przykład z innej odpowiedzi):
void foo() {
std::string str;
// Do cool things to or using str
}
Działa to dobrze - ale co, jeśli chcemy zwrócić str? Możemy to napisać:
std::string foo() {
std::string str;
// Do cool things to or using str
return str;
}
Co jest z tym nie tak? Typem zwracanym jest std :: string - więc oznacza to, że zwracamy wartość. Oznacza to, że kopiujemy str i faktycznie zwracamy kopię. Może to być kosztowne i możemy chcieć uniknąć kosztów jego kopiowania. Dlatego możemy wymyślić pomysł powrotu przez odniesienie lub wskaźnik.
std::string* foo() {
std::string str;
// Do cool things to or using str
return &str;
}
Niestety ten kod nie działa. Zwracamy wskaźnik do str - ale str został utworzony na stosie, więc jesteśmy usuwani po wyjściu z foo (). Innymi słowy, zanim dzwoniący otrzyma wskaźnik, jest bezużyteczny (i prawdopodobnie gorszy niż bezużyteczny, ponieważ jego użycie może powodować różnego rodzaju błędy funkcyjne)
Więc jakie jest rozwiązanie? Możemy utworzyć str na stercie za pomocą new - w ten sposób, gdy foo () zostanie zakończone, str nie zostanie zniszczony.
std::string* foo() {
std::string* str = new std::string();
// Do cool things to or using str
return str;
}
Oczywiście to rozwiązanie również nie jest idealne. Powodem jest to, że utworzyliśmy str, ale nigdy go nie usuwamy. Może to nie być problemem w bardzo małym programie, ale ogólnie chcemy się upewnić, że go usunęliśmy. Moglibyśmy po prostu powiedzieć, że osoba dzwoniąca musi usunąć obiekt, gdy już go skończy. Minusem jest to, że osoba dzwoniąca musi zarządzać pamięcią, co powoduje dodatkową złożoność i może ją pomylić, co prowadzi do wycieku pamięci, tj. Nie usuwa obiektu, nawet jeśli nie jest już wymagany.
W tym miejscu pojawiają się inteligentne wskaźniki. W poniższym przykładzie użyto shared_ptr - sugeruję, aby spojrzeć na różne typy inteligentnych wskaźników, aby dowiedzieć się, czego faktycznie chcesz użyć.
shared_ptr<std::string> foo() {
shared_ptr<std::string> str = new std::string();
// Do cool things to or using str
return str;
}
Teraz shared_ptr policzy liczbę referencji do str. Na przykład
shared_ptr<std::string> str = foo();
shared_ptr<std::string> str2 = str;
Teraz są dwa odniesienia do tego samego ciągu. Gdy nie będzie już żadnych odniesień do str, zostanie on usunięty. W związku z tym nie musisz się już martwić samodzielnym usunięciem.
Szybka edycja: jak zauważyły niektóre komentarze, ten przykład nie jest idealny z (przynajmniej!) Dwóch powodów. Po pierwsze, ze względu na implementację ciągów, kopiowanie ciągu jest zwykle niedrogie. Po drugie, ze względu na tak zwaną optymalizację nazw zwracanych, zwracanie według wartości może nie być drogie, ponieważ kompilator potrafi sprytnie przyspieszyć.
Wypróbujmy inny przykład, korzystając z naszej klasy File.
Powiedzmy, że chcemy użyć pliku jako dziennika. Oznacza to, że chcemy otworzyć nasz plik w trybie tylko dołączania:
File file("/path/to/file", File::append);
// The exact semantics of this aren't really important,
// just that we've got a file to be used as a log
Teraz ustawmy nasz plik jako dziennik dla kilku innych obiektów:
void setLog(const Foo & foo, const Bar & bar) {
File file("/path/to/file", File::append);
foo.setLogFile(file);
bar.setLogFile(file);
}
Niestety, ten przykład kończy się okropnie - plik zostanie zamknięty, gdy tylko zakończy się ta metoda, co oznacza, że foo i bar mają teraz nieprawidłowy plik dziennika. Możemy zbudować plik na stercie i przekazać wskaźnik do pliku zarówno do foo, jak i do paska:
void setLog(const Foo & foo, const Bar & bar) {
File* file = new File("/path/to/file", File::append);
foo.setLogFile(file);
bar.setLogFile(file);
}
Ale kto jest odpowiedzialny za usunięcie pliku? Jeśli żaden plik nie zostanie usunięty, mamy przeciek pamięci i zasobów. Nie wiemy, czy plik foo czy bar skończy się najpierw na pliku, więc nie możemy oczekiwać, że sam usunie plik. Na przykład, jeśli foo usunie plik, zanim pasek się z nim skończy, pasek ma teraz nieprawidłowy wskaźnik.
Jak zapewne zgadliście, moglibyśmy użyć inteligentnych wskaźników, aby nam pomóc.
void setLog(const Foo & foo, const Bar & bar) {
shared_ptr<File> file = new File("/path/to/file", File::append);
foo.setLogFile(file);
bar.setLogFile(file);
}
Teraz nikt nie musi się martwić usunięciem pliku - gdy zarówno foo, jak i pasek zakończą się i nie będą już miały żadnych odniesień do pliku (prawdopodobnie z powodu zniszczenia foo i paska), plik zostanie automatycznie usunięty.
RAII To dziwna nazwa prostej, ale niesamowitej koncepcji. Lepsza jest nazwa Scope Bound Resource Management (SBRM). Chodzi o to, że często zdarza się, że alokujesz zasoby na początku bloku i musisz go zwolnić przy wyjściu z bloku. Wyjście z bloku może nastąpić przez normalną kontrolę przepływu, wyskakując z niego, a nawet w drodze wyjątku. Aby uwzględnić wszystkie te przypadki, kod staje się bardziej skomplikowany i zbędny.
Tylko przykład robienia tego bez SBRM:
Jak widzisz, istnieje wiele sposobów, w jakie możemy zostać przekonani. Chodzi o to, że enkapsulujemy zarządzanie zasobami w klasę. Inicjalizacja obiektu pozyskuje zasób („Pozyskiwanie zasobów to inicjalizacja”). Kiedy wychodzimy z bloku (zakres bloku), zasób jest ponownie zwalniany.
To dobrze, jeśli masz własne klasy, które nie służą wyłącznie do alokacji / dezalokacji zasobów. Alokacja byłaby tylko dodatkową troską w wykonaniu ich pracy. Ale gdy tylko chcesz przydzielić / cofnąć przydział zasobów, powyższe staje się nieprzydatne. Musisz napisać klasę zawijania dla każdego rodzaju zasobów, które zdobędziesz. Aby to ułatwić, inteligentne wskaźniki pozwalają zautomatyzować ten proces:
Zwykle inteligentne wskaźniki są cienkimi opakowaniami wokół nowego / usuwania, które po prostu wywołują,
delete
gdy zasób, którego są właścicielami, wykracza poza zakres. Niektóre inteligentne wskaźniki, takie jak shared_ptr, pozwalają im powiedzieć tak zwany deleter, który jest używany zamiastdelete
. Pozwala to na przykład zarządzać uchwytami okien, zasobami wyrażeń regularnych i innymi dowolnymi rzeczami, pod warunkiem, że powiesz shared_ptr o właściwym usuwaczu.Istnieją różne inteligentne wskaźniki do różnych celów:
Unique_ptr
jest inteligentnym wskaźnikiem, który jest właścicielem wyłącznie obiektu. Nie działa, ale prawdopodobnie pojawi się w następnym standardzie C ++. Nie można go kopiować, ale obsługuje przeniesienie własności . Przykładowy kod (następny C ++):Kod:
W przeciwieństwie do auto_ptr, unikalny_ptr można umieścić w kontenerze, ponieważ kontenery będą mogły przechowywać typy niemożliwe do kopiowania (ale ruchome), takie jak strumienie i unikalne_ptr.
scoped_ptr
to inteligentny wskaźnik doładowania, którego nie można kopiować ani przenosić. Jest to idealna rzecz do użycia, gdy chcesz mieć pewność, że wskaźniki zostaną usunięte, gdy wyjdziesz poza zakres.Kod:
shared_ptr
jest dla współwłasności. Dlatego jest on zarówno do kopiowania, jak i do przenoszenia. Wiele instancji inteligentnego wskaźnika może posiadać ten sam zasób. Gdy tylko ostatni inteligentny wskaźnik będący właścicielem zasobu znajdzie się poza zasięgiem, zasób zostanie zwolniony. Oto przykład jednego z moich projektów w świecie rzeczywistym:Kod:
Jak widać, źródło wydruku (funkcja fx) jest współdzielone, ale każdy z nich ma osobny wpis, w którym ustawiamy kolor. Istnieje klasa słaba_ptr, która jest używana, gdy kod musi odwoływać się do zasobu będącego własnością inteligentnego wskaźnika, ale nie musi być właścicielem zasobu. Zamiast przekazywać nieprzetworzony wskaźnik, powinieneś utworzyć słaby_ptr. Zgłasza wyjątek, gdy zauważy, że próbujesz uzyskać dostęp do zasobu za pomocą ścieżki dostępu słaby_ptr, nawet jeśli nie ma już zasobu współużytkowanego_ptr.
źródło
unique_ptr
, isort
zostaną również zmienione.Założenie i powody są proste, w koncepcji.
RAII jest paradygmatem projektowym zapewniającym, że zmienne obsługują wszystkie potrzebne inicjalizacje w swoich konstruktorach i wszystkie potrzebne porządki w swoich destrukterach. Zmniejsza to całą inicjalizację i czyszczenie do jednego kroku.
C ++ nie wymaga RAII, ale coraz częściej przyjmuje się, że użycie metod RAII da bardziej niezawodny kod.
Powodem, dla którego RAII jest użyteczne w C ++ jest to, że C ++ wewnętrznie zarządza tworzeniem i niszczeniem zmiennych, gdy wchodzą one i wychodzą z zasięgu, czy to poprzez normalny przepływ kodu, czy poprzez odwijanie stosu wywołane przez wyjątek. To darmowy program w C ++.
Wiążąc całą inicjalizację i czyszczenie z tymi mechanizmami, masz pewność, że C ++ zajmie się tą pracą również dla Ciebie.
Mówienie o RAII w C ++ zwykle prowadzi do dyskusji na temat inteligentnych wskaźników, ponieważ wskaźniki są szczególnie delikatne, jeśli chodzi o czyszczenie. Podczas zarządzania pamięcią alokowaną na stercie pozyskanej z malloc lub nowej, programista zwykle ma obowiązek zwolnić lub usunąć tę pamięć przed zniszczeniem wskaźnika. Inteligentne wskaźniki wykorzystają filozofię RAII, aby zapewnić zniszczenie obiektów przydzielonych na stosie za każdym razem, gdy niszczona jest zmienna wskaźnika.
źródło
Inteligentny wskaźnik jest odmianą RAII. RAII oznacza, że pozyskiwanie zasobów jest inicjalizacją. Inteligentny wskaźnik pobiera zasób (pamięć) przed użyciem, a następnie wyrzuca go automatycznie w destruktorze. Stają się dwie rzeczy:
Na przykład innym przykładem jest gniazdo sieciowe RAII. W tym przypadku:
Teraz, jak widać, RAII jest bardzo przydatnym narzędziem w większości przypadków, ponieważ pomaga ludziom się położyć.
Źródła C ++ inteligentnych wskaźników są w milionach w sieci, w tym odpowiedzi nade mną.
źródło
Boost ma wiele z nich, w tym te w Boost.Interprocess dla pamięci współużytkowanej. Znacznie upraszcza zarządzanie pamięcią, szczególnie w sytuacjach wywołujących ból głowy, takich jak 5 procesów współużytkujących tę samą strukturę danych: gdy wszyscy mają dość pamięci, chcesz, aby automatycznie się uwolniła i nie musiała tam siedzieć, próbując rozgryźć kto powinien być odpowiedzialny za wywołanie
delete
fragmentu pamięci, aby nie skończył się wyciekiem pamięci lub wskaźnikiem, który zostanie dwukrotnie uwolniony i może uszkodzić całą stertę.źródło
Bez względu na to, co się stanie, pasek zostanie poprawnie usunięty po pozostawieniu zakresu funkcji foo ().
Wewnętrzne implementacje std :: string często używają wskaźników liczonych w referencjach. Zatem wewnętrzny ciąg musi zostać skopiowany tylko wtedy, gdy zmieni się jedna z kopii ciągów. Dlatego inteligentny wskaźnik zliczony z odniesieniem umożliwia kopiowanie tylko w razie potrzeby.
Ponadto wewnętrzne zliczanie referencji umożliwia prawidłowe usunięcie pamięci, gdy kopia wewnętrznego łańcucha nie jest już potrzebna.
źródło