C++11: Wskaźniki unikalne i współdzielone (unique_ptr, shared_ptr)

Programowanie

Nowe standardy języka C++ dodają nie tylko całkiem nowe możliwości, ale także udostępniają nam udogodnienia, dzięki którym możemy wyeliminować na zawsze niektóre problemy, które były poważnym utrapieniem dla programistów. Jednym z takich problemów są wycieki pamięci przy jej dynamicznym alokowaniu. Wielu z nas często zapomina o zwalnianiu (przy użyciu operatorów delete i delete[]) pamięci zarezerwowanej przy pomocy new oraz new[] . Nowe udogodnienia w postaci wskaźnikówunikalnych iwspółdzielonych powodują, że z operatora delete będziemy korzystać coraz rzadziej. Czym właściwie są nowe rodzaje wskaźników?

Wspólne cechy wskaźników unikalnych i współdzielonych

Zacznijmy od tego, że nie są to nowe konstrukcje języka, a jedynie szablony klasy z biblioteki szablonów STL (Standard Template Library). Chodzi konkretnie o std::unique_ptr i std::shared_ptr. Szablony te wymagają co najmniej jednego typu - typu danych na jakie wskaźnik ma pokazywać. Drugim przekazywanym parametrem szablonu może być specjalna klasa obiektów funkcyjnych, która zwalnia pamięć po obiekcie. Standardowo jest używany default_deletez biblioteki standardowej - czyli obiekt, który wykorzystuje wbudowany w język operator delete. Aby wykorzystywać te typy wskaźników należy dołączyć do projektu plik nagłówkowy memory #include <memory>.

Obiekty klas unque_ptr i shared_ptr w praktyce

Dzięki przeładowaniu odpowiednich operatorów (operator->, operator*, operator= i operator bool) możemy korzystać z nowych wskaźników tak samo jak z tych, które są konstrukcją języka. Dodatkowo możemy uzyskać dostęp do zwyczajnego wskaźnika dzięki funkcji składowej get() (obecnej zarówno w std::unique_ptr jak i std::shared_ptr). Dzięki temu wskaźnik możemy wykorzystywać również w przypadku gdy wymagane jest wykorzystywanie tradycyjnych wskaźników. Teraz przejdźmy do tego, czym różnią się typy std::unique_ptr i std::shared_ptr.

Wskaźniki unikalne - unique_ptr

Zaczniemy od wskaźników unikalnych. Ich cechy charakterystyczne to:

  • może istnieć tylko jeden wskaźnik tego typu pokazujący na dany obiekt,
  • w momencie kiedy przestaje istnieć wskaźnik, usuwany jest także obiekt, na który ten wskaźnik pokazywał, a pamięć, którą zajmował jest zwalniana,
  • obiekt na który wskazuje wskaźnik jest także usuwany kiedy do wskaźnika przypisujemy inny obiekt,
  • obiekt wskazywany przez unique_ptr nie może być zarządzany przez inny obiekt tego typu ani też shared_ptr.

Przykład użycia wskaźników unikalnych:

 1 #include <iostream>
 2 #include <memory>
 3 
 4 using namespace std;
 5 
 6 struct MyClass
 7 {
 8   static int nextId;
 9   int id;
10   MyClass() : id(++nextId)
11   {
12     cout << "Tworze obiekt klasy MyClass. id = " << id << "\n";
13   }
14   ~MyClass()
15   {
16     cout << "Usuwam obiekt klasy MyClass. id = " << id << "\n";
17   }
18   void f()
19   {
20     cout << "Wywołanie funkcji f() dla obiektu id: " << id << "\n";
21   }
22 };
23 
24 struct MyClassDeleter
25 {
26   void operator()(MyClass* ptr)
27   {
28     cout << "--- Uruchomienie usuwania obiektu id: "
29          << ptr->id << " przy pomocy MyClassDeleter\n";
30     delete ptr;
31     cout << "--- Obiekt id: " << ptr->id << " usuniety\n";
32   }
33 };
34 
35 int MyClass::nextId = 0;
36 
37 int main()
38 {
39   unique_ptr<MyClass> ptr1(new MyClass);
40   ptr1->f();
41   (*ptr1).f();
42   unique_ptr<MyClass, MyClassDeleter> ptr2(new MyClass);
43   return 0;
44 }

W powyższym przykładzie zdefiniowaliśmy klasę (właściwie strukturę) MyClass, która posiada konstruktor i destruktor oraz funkcję składową void f(). Każda z tych funkcji wyświetla stosowny komunikat. Kolejna struktura to "usuwacz". Kiedy wskaźnik ma usunąć obiekt, wykorzystuje obiekt tej struktury jako funkcję (czyli uruchamia operator()). W funkcji main() tworzymy dwa unikalne wskaźniki i korzystamy z funkcji MyClass::f(). Po uruchomieniu programu widzimy, kiedy tworzone są obiekty, kiedy wywoływane ich funkcje składowe i kiedy są one usuwane z pamięci. Jak widać, do tego ostatniego nie przyłożyliśmy nawet ręki, a pamięć została zwolniona.

Wskaźniki współdzielone - shared_ptr

Cechy charakterystyczne wskaźników współdzielonych to:

  • może istnieć wiele wskaźników do tego samego obiektu
  • obiekt jest usuwany w momencie gdy przestaje istnieć ostatni wskaźnik typu shared_ptr

Przykład użycia wskaźników współdzielonych:

 1 #include <iostream>
 2 #include <memory>
 3 
 4 using namespace std;
 5 
 6 struct MyClass
 7 {
 8   static int nextId;
 9   int id;
10   MyClass() : id(++nextId)
11   {
12     cout << "Tworze obiekt klasy MyClass. id = " << id << "\n";
13   }
14   ~MyClass()
15   {
16     cout << "Usuwam obiekt klasy MyClass. id = " << id << "\n";
17   }
18   void f()
19   {
20     cout << "Wywołanie funkcji f() dla obiektu id: " << id << "\n";
21   }
22 };
23 
24 int MyClass::nextId = 0;
25 
26 int main()
27 {
28   shared_ptr<MyClass> ptr1(new MyClass);
29   cout << "Tworze 2 kolejne wskazniki shared_ptr do obiektu o id: "
30        << ptr1->id << "\n";
31   shared_ptr<MyClass> ptr2(ptr1);
32   shared_ptr<MyClass> ptr3(ptr1);
33 
34   cout << "Uzywam funkcji f we wszystkich wskaźnikach\n";
35 
36   ptr1->f();
37   (*ptr2).f();
38   ptr3->f();
39 
40   cout << "Program usuwa wskazniki\n";
41 
42   return 0;
43 }

Ponieważ std::shared_ptr nie udostępnia możliwości ustawienia klasy kasującej obiekt, wykorzystamy tutaj tylko MyClass. Na samym początku tworzymy jeden wskaźnik współdzielony do nowoego obiektu zaalokowanego w pamięci. Następnie tworzymy dwa współdzielone wskaźniki. Musimy pamiętać, aby każdy następny tworzyć nie z tradycyjnego wskaźnika do pamięci, a z istniejącego już współdzielonego wskaźnika do pamięci. Następnie wykonujemy funkcję MyClass::f() wykorzystując operator*, służący do wyłuskania obiektu (uzyskania do niego bezpośredniego dostępu) i operator-> służący do wyłuskania funkcji składowej tego obiektu.