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?
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?
Odpowiedzi:
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:
void o_really() {
resource * r = allocate_resource();
try {
// something, which could throw. ...
} catch(...) {
deallocate_resource(r);
throw;
}
if(...) { return; } // oops, forgot to deallocate
deallocate_resource(r);
}
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.
struct resource_holder {
resource_holder() {
r = allocate_resource();
}
~resource_holder() {
deallocate_resource(r);
}
resource * r;
};
void o_really() {
resource_holder r;
// something, which could throw. ...
if(...) { return; }
}
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:
shared_ptr<Entry> create_entry(Parameters p) {
shared_ptr<Entry> e(Entry::createEntry(p), &Entry::freeEntry);
return e;
}
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 zamiast delete
. 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:
Kod:
unique_ptr<plot_src> p(new plot_src); // now, p owns
unique_ptr<plot_src> u(move(p)); // now, u owns, p owns nothing.
unique_ptr<plot_src> v(u); // error, trying to copy u
vector<unique_ptr<plot_src>> pv;
pv.emplace_back(new plot_src);
pv.emplace_back(new plot_src);
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.
Kod:
void do_something() {
scoped_ptr<pipe> sp(new pipe);
// do something here...
} // when going out of scope, sp will delete the pointer automatically.
Kod:
shared_ptr<plot_src> p(new plot_src(&fx));
plot1->add(p)->setColor("#00FF00");
plot2->add(p)->setColor("#FF0000");
// if p now goes out of scope, the src won't be freed, as both plot1 and
// plot2 both still have references.
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.
unique_ptr
, i sort
zostaną również zmienione.
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.
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ą.
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ę.
void foo () { std :: string bar; // // więcej kodu tutaj // }
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.