Ten wpis jest skierowany do nowicjuszy i jest odpowiedzią na nagminną praktykę uczenia C++ jako C with classes (witamy w roku 1984), skutkującą potem zalewem kodu fatalnej jakości na wszelkiej maści forach.
Kiedy? NIGDY!
Ok, jest to pewne uproszczenie i są od tej reguły wyjątki. Ale chciałbym wyraźnie przekazać, że to użycie new jest odstępstwem od reguły, a nie odwrotnie. W przypadku delete takich wyjątków nie ma wcale.
To jak mam utworzyć obiekt?
C++ to nie Java. Poniższy kod nie jest niejawną definicją wskaźnika na Foo, tylko obiektu tego typu:
Foo foo; |
Tutaj wstawię link do wyjaśnienia czym jest wskaźnik/referencja/obiekt/klasa, gdy takie napiszę.
Chcę mieć tablicę o rozmiarze nieznanym w czasie kompilacji
Super. Od tego jest std::vector. Dodatkowo dzięki RAII nie musisz martwić się o zwolnienie zasobów (zero- lub wielokrotne).
int* arr = new T[n]; for(int i = 0; i < n; ++i) { arr[i] = process(i); } display(arr, n); delete[] arr; |
W powyższym kodzie, jeśli display lub process rzuci wyjątek, pamięć arr wycieknie. Jeśli w procesie refaktoringu zostanie dodane nowe wyjście z funkcji, programista będzie musiał pamiętać aby dodać odpowiednią komendę delete[]. Wszystko to jest zbędne, gdy pozwolisz kompilatorowi się wyręczyć za sprawą RAII. Analogiczne użycie std::vector może wyglądać następująco:
std::vector<int> arr(n); for(int i = 0; i < n; ++i) { arr[i] = process(i); } display(arr, n); |
Opisane wyżej problemy go nie dotyczą.
A co jeśli chcę mieć dostęp do tego samego obiektu z wielu miejsc/obiekt jest duży i jego wielokrotne kopiowanie jest złe?
Nic nie stoi na przeszkodzie, aby przekazać referencję albo wskaźnik do obiektu o automatycznym czasie życia (kolokwialnie: znajdującego się na stosie). Można tez użyć std::unique_ptr/std::shared_ptr wraz z std::make_unique/std::make_shared.
Najistotniejszą sprawą jest jasne określenie kto, gdzie i w jaki sposób odpowiada za czas życia, a więc i usunięcie obiektu. W większości przypadków wystarczy automatyczny czas życia, czyli “zwykła zmienna” i to powinno być rozwiązanie domyślne:
void process_ptr(Foo const*); void process_ref(Foo const&); // ... Foo foo; process_ptr(&foo); process_ref(foo); |
Jeśli z jakiegoś powodu nie jest możliwe użycie takiego sposobu, pierwszą alternatywą powinien być std::unique_ptr. Może tak być np. jeśli chcemy dynamicznie zadecydować o typie obiektu lub jeśli jakieś API zwraca wskaźnik do obiektu, za którego usunięcie odpowiada użytkownik:
void process_ptr(Widget const*); void process_ref(Widget const&); // ... std::unique_ptr<Widget> someWidget{createSomeWidget()}; process_ptr(someWidget.get()); process_ref(*someWidget); |
Dopiero gdy użycie std::unique_ptr okaże się niemożliwe powinien zostać rozważony std::shared_ptr. Dzieje się tak najczęściej gdy nie ma jasno określonego czasu życia obiektu. Pozwala on na utrzymanie go przy życiu dopóki istnieje chociaż jeden std::shared_ptr na dany obiekt.
Ten wybór, wraz z życiowymi przykładami został fantastycznie przedstawiony przez Herba Suttera na ostatnej konferencji C++Con:
Ok, to kiedy mogę użyć new?
Znam dwa akceptowalne powody:
Używasz jakiegoś frameworka/biblioteki, który to wymusza
Np. Qt, gdzie częstym widokiem jest:
QSomeWidget* w = new QSomeWidget(this); |
Należy jednak zauważyć, że w powyższym przypadku QWidget przejmuje odpowiedzialność za zwolnienie zaalokowanego obiektu w odpowiednim momencie. Nie ma więc potrzeby samodzielnego wywoływania delete.
make_shared/make_unique nie jest w stanie skonstruować obiektu
Jest tak np. dla agregatów (w pewnym uproszczeniu klasa lub tablica klas bez konstruktorów niedomyślnych, zawierająca tylko publiczne elementy), które trzeba inicjalizować za pomocą {}. Dlatego poniższy kod jest akceptowalny:
struct person { std::string name; int number_of_hands; }; auto ptr = std::unique_ptr<person>(new person{"Jan Kowalski", 2}); |
A delete?
Nigdy. Serio, we wszystkich wyżej opisanych przypadkach nie ma potrzeby ręcznego pisania delete. Jeśli Twój kod jest napisany w taki sposób, że jest to niezbędne – robisz coś źle.
A co jeśli nie jestem nowicjuszem?
W takim wypadku wszystko powyższe powinieneś już wiedzieć i stosować. Jedyne dodatkowe przypadki, gdzie sensowne jest ręczne użycie new to implementacja własnych kontenerów/smart pointerów lub alokatorów (wtedy również ma sens użycie delete) lub placement new. Ewentualnie new (std::nothrow) T jako argument konstruktora smart pointera.
Podsumowując
Użycie nagich new/delete jest niezalecane. Jeśli jesteś nowicjuszem: stosuj się do tych rad. W przeciwnym wypadku zdaj się na własne doświadczenie. Temat ten poruszyłem także w tym artykule.
Bardzo wartościowy i przydatny artykuł dla początkujących 🙂
Artykuł ciekawy i pouczający, chociaż dla mnie trochę zdawkowy. Chętnie poczytam coś więcej na ten temat, czy polecasz jakąś książkę, która pozwala uczyć się C++ od podstaw bez wpadania w złe nawyki, m.in. takie, jak ten opisany powyżej?
Później jeszcze przejrzę i postaram się zmniejszyć zdawkowość 🙂
Jeśli chodzi o książki to ta lista jeszcze mnie nie zawiodła, chociaż pozycje dla nowicjuszy co najwyżej przejrzałem.
Ok, a co w takim przypadku: załóżmy że pisze np grę.
Mam klasę GameObject, po której dziedziczy Sprite, Object3d i dajmy na to LightSource
Teraz żeby móc trzymać wszystkie obiekty w jednym miejscu i się wygodnie iterować robię
std::vector vec;
i wrzucanie na taki wektor wymaga użycia new a potem delete w usuwaniu z wektora.
Jestem ciekaw czy da się mieć podobną funkcjonalność bez użycia new/delete?
Trzymaj wtedy std::vector<std::unique_ptr<GameObject>>. Przy okazji nie będziesz musiał się bawić w ręczne wołanie delete 🙂 Ewentualnie, ostatnio pojawiają się opinie aby tego typu kolekcje trzymać jakos wektor wariantów (std::variant/boost::variant), ale to rozwiązanie sensowne dopiero od C++14/17.
Aha, no i ogólnie w tym przypadku sensowne też może być boost::ptr_vector.
Napisałem gierkę, masa w niej błędów i wycieków pamięci, jak ktoś chce się pobawić w odgruzowywanie, przeróbki to proszę bardzo: http://microgeek.eu/viewtopic.php?f=59&t=827&p=5581
Uważam że kod jest w miarę przejrzysty, trzeba by utworzyć nową klasę player i do niej przenieść gracza.
Thanks, great article.
Dziękuję. Przydało mi się. Niestety przesiadka z C#/Javy na C++ bywa trudna;)
A propos “display lub process rzuci wyjątek” – ale C++ chyba też ma obsługę wyjątków, co nie? Można obsłużyć delete w catchu…
Można. Ale problem jest taki, że ten catch (a więc i try) musi być wewnątrz tej funkcji, ponieważ jeśli będzie kilka wywołań wyżej to nie będziesz miał wskaźnika dla delete. Więc w takim przypadku musiałbyś ręcznie dodać try/catch dla każdego wywołania funkcji, która może rzucić wyjątek, w miejscu gdzie masz zaalokowane zasoby (tutaj pamięć). Jest to możliwe, ale szalenie niepraktyczne i i tak łatwo o pomyłkę (np. jeśli wskaźników do zwolnienia jest więcej, rodzajów wyjątków jest więcej, i jeśli jest kilka stopni zagłębienia try/catchów w funkcji 😉 )
Zamiast się tak męczyć można sięgnąć po idiomatyczne rozwiązania i pozwolić kompilatorowi się wyręczyć w sposób bezpieczny i wydajny. Dzięki temu w dobrym kodzie C++ błędy typu goto fail nie mają prawa mieć miejsca.
Jestem początkujący ale chyba za bardzo 🙂 wiem tylko z jakiś kursów internetowych, że new służy do tworzenia nowego obiektu danej klasy a delete kasuje tablicę utworzoną dynamicznie resztę słabo ogarniam z tego wpisu al e ucze się sam, mimo wszystko dzięki za artykuł i proszę o coś prostszego dla początkujących
np: to co pisałeś ‘Tutaj wstawię link do wyjaśnienia czym jest wskaźnik/referencja/obiekt/klasa, gdy takie napiszę.’
Te kursy są przestarzałe (albo niedokładnie je cytujesz 😉 ) Za pomocą new faktycznie możesz utworzyć nowy obiekt, i taki obiekt faktycznie powinien zostać usunięty za pomocą delete. Rzecz w tym, że, tak jak opisałem wyżej, jest to dość niebezpieczne, nakłada na programistę zbędne obowiązki nie oferując nic w zamian. Dlatego też jest to złą praktyką, zamiast której powinno się stosować zamienniki, takie jak smart pointery (głównie unique_ptr) i kontenery (std::vector jako domyślny).
Nie wiem jak umieścić Ci tutaj kod. Pewnie się posypie..
Jak oceniłbyś dwa przypadki, gdy `new` potencjalnie może być niezbędne:
– placement `new` do alokacji danych w puli pamięci (z grubsza: alokujesz na stercie pulę pamięci np. na 1000 obiektów, a placement `new` ‘dobiera’ z tejże puli kolejne bajty pamięci; po skończeniu pracy z pulą – orasz całość)
– gdy Twoja aplikacja ma jak najszybciej wykonać jakiś kawałek kodu, a potem skonać – nie interesuje Cię “odzyskanie pamięci”, bo całość i tak zaraz zginie, ale z jakiegoś powodu nie możesz wrzucić danych na stos
Przede wszystkim: tekst jest skierowany do nowicjuszy, więc takich tematów nawet nie podejmowałem.
Placement new można rozumieć jako utworzenie/inicjalizację obiektu lub wywołanie konstruktora. W przykładzie opisałeś coś zbliżonego do standardowego wektora 😉 Mój główny problem jest ze “wskaźnikami posiadającymi”, w przypadku których ich użytkownik jest odpowiedzialny za zwolnienie zasobów. W przypadku placement new ten problem nie musi występować. Ale mimo wszystko jest to nieskopoziomowe narzędzie, którego używałbym z rozwagą. Rozmyślając na szybko, nie jestem w stanie wymyślić sensownego zastosowania placement new poza pisaniem własnego kontenera.
W przypadku aplikacji “run and die”, faktycznie może to być szybsze (citation & benchmark needed). O ile dobrze pamiętam, GCC działa/działało w ten sposób. W przypadku samego kompilatora jest to sensowne, ale jest równocześnie jednym z powodów nieistnienia gcc jako biblioteki, w przeciwieństwie do clanga/LLVM. Inaczej mówiąc, jeśli dla Twojego use-case’u ma to sens, to nie widzę przeszkód; ale to powinna być świadoma decyzja, inaczej może spowodować problemy w przyszłości.
Fajny artykul. Na pewno sie przyda poczatkujacym.
Wkradl sie jednak blad:
“Dzieje się tak najczęściej gdy nie ma jasno określonego czasu życia obiektu. Pozwala on na utrzymanie go przy życiu dopóki istnieje chociaż jeden std::unique_ptr na dany obiekt.”
Moim zdaniem powinno byc: “dopóki istnieje chociaż jeden std::shared_ptr na dany obiekt.” Istnienie wiecej niz jednego unique_ptr dla tego samego obiektu jest bledem. Nazwa unique_ptr o tym swiadczy oraz fakt, ze unique_ptr ma move constructor a nie ma copy constructora.
Faktycznie, dzięki za spostrzeżenie! Poprawione.
Właśnie użyłem delete 🙂
Pozdrawiam.