Czy wciąż używać wskaźników surowych?

Jedną z największych pułapek w jakie można wpaść po poznaniu wskaźników inteligentnych, jest stwierdzenie, że od tej pory nie używamy już wskaźników surowych. Świetnie wytłumaczył to Herb Sutter lecz myślę, że temat ten wciąż nie pojawia się wystarczająco często. Z tego powod postanowiłem zawrzeć w tym artykule pigułkę tego co powiedział już Herb.

Jeżeli nie oglądałeś wcześniej jego wykładu i nie masz na to obecnie czasu, szybkie zapoznanie się z tematem może być dla Ciebie wygodniejszym rozwiązaniem- szczególnie, jeżeli nie czujesz się jeszcze swobodnie z językiem angielskim w programowaniu.

Podczas poznawania wskaźników inteligentnych, często zwracalismy uwage na to czy posiadają one wskazywany zasób na własność, czy może współdzielą go z innymi wskaźnikami lub jedynie sprawdzają czy nadal on istnieje. W zasadzie jest to jedna z najważniejszych rzeczy związanych ze wskaźnikami inteligentnymi- ich deklaracja jasno informauje nas o tym, czy posiadamy obiekt na który wskazujemy- raw pointer tego nie robi. Powstaje jednak pytanie, czy zawsze potrzebujemy wiedzieć, że obiekt posiadamy na właność? Spójrzmy na kod poniżej:

void set_percent_value_to_widget_by_uptr(unique_ptr<Widget> & p_widget, unsigned value)
{
    if(value > 100)
    {
        value = 100;
    }
    
    p_widget->set_val(value);
}

void set_percent_value_to_widget_by_ref(Widget & widget, unsigned value)
{
    if(value > 100)
    {
        value = 100;
    }
    
    widget.set_val(value);
}


int main()
{
    unique_ptr<Widget> u_ptr{new Widget};
    set_percent_value_to_widget_by_uptr(u_ptr, 101);
    cout << *u_ptr << endl;
    set_percent_value_to_widget_by_ref(*u_ptr, 6);
    cout << *u_ptr << endl;
}

W naszym programie zdefiniowaliśmy dwie funkcje- set_percent_value_to_widget_by_uptr oraz set_percent_value_to_widget_by_ref. Obie realizują to samo zadanie- przyjmują jako argument widget (za pomocą unique_ptr lub za pomocą referencji) a następnie zmieniają w nim wartość na tę podaną w drugim argumencie (z zakresu [0;100] co odpowiada procentom). 

W głównej częsci naszego programu tworzymy unique_ptr, który przechowuje Widget. Następnie chcemy ustawić w nim określoną wartość. Powstaje pytanie, ktora z funkcji nadaje się do tego zadania lepiej? Używamy unique_ptr więc intuicyjnie możemy powiedzieć, że chcemy użyć funkcji, która przyjmuje taki argument. W końcu od niedawna używamy wskaźników inteligentnych, które mają tyle plusów, więc naturalnie myślimy o użyciu ich i teraz. Rodzi się jednak pytanie, po co przekazywać do funkcji ustawiającej wartość Widgetu cały unique_ptr i wszystkie zawarte w nim informacje? Programujemy w C++11, znamy wskaźniki inteligentne, wiemy, że jeżeli chcemy przekazać komuś prawo własności nad obiektem, przekazujemy wskaźnik inteligentny- on informuje odbiorcę o tym, że dostał obiekt na właność. Tutaj jendak nie chcemy przekazać wcale własności. Chcemy dać funkcji obiekt, który zostanie w niej zmodyfikowany. Funkcja ta nie potrzebuje wiedzieć czy posiada ten obiekt na właność, czy jest on dzielony między wielu właścicieli etc. Zadaniem funkcji jest dokonanie określonej operacji na obiekcie i wiedza o tym kto jest właścicielem tego obiektu jest tutaj zbędna. Dlatego nie powinniśmy zaciemniać kodu zbędnymi danymi i w brew pozorom najlepszym rozwiązaniem okazuje się przekazanie referencji.

Dzięki referencji funkcja wykona swoją pracę, a nasz program nie zmieniając właściciela obiektu wykona się poprawnie. Dodatkowo jeżeli argumet jest opcjonalny, przekażemy w tym przypadku wskaźnik surowy do obiektu (czyli u_ptr.get()) (opcjonalne argumenty przesyłamy za pomocą wskaźnika, który w przypadku braku argumentu jest nullptr'em). To istotne. Wskaźnik inteligentny przesyłamy, gdy chcemy powiedzieć komuś jawnie- od tej pory to ty posiadasz prawa do tego obiektu, zarządzaj cała związaną z tym logiką. Jeżeli jednak określona funkcja ma dokonać prostej modyfikacji obiektu, nie musi nic wiedzieć o czasie życia obiektu. Gdzieś u podnóża stosu istnieje kod, który wywołuje tę funkcję i panuję nad tym jak długo ma żyć modyfikowany obiekt. Ta konkretna funkcja ma zrobić swoją robotę i nic więcej. Surowy wskaźnik nadaje się do tego idealnie- o ile pamiętamy, ze na surowym wskaźnikach nie możemy wołać operatora delete! Ale to już przecież wiemy ;) 

Raw pointers oraz referencje pozostają domyślnymi argumentami funkcji. Zupełnie jak w C++98, czyli przed wskaźnikami inteligentnymi. Dopiero gdy chcemy przekazać informacje o własności nad obiektem używamy wskaźników inteligentnych. Stąd faktorka zwróci nam unique_ptr informując nas- Słuchaj, od teraz to ty zarządzasz życiem tego obiektu a ja umywam od tego ręce! Natomiast prosta funkcja przyjmuje od nas referencje tak jak w dawnym C++98 bo do wykonania jej pracy wcale nie potrzebna jest posiadanie obiektu na własność. 

Temat wydaje się prosty, zwykle trudno jednak dojść do tych wniosków samemu. Na szczęscie Herb dobrze wytłumaczył to podczas swojego wykładu- raz jeszcze gorąco Cię do niego odsyłam gdy znajdziesz nieco więcej czasu. A póki co mam nadzieję, że ten krótki wpis nieco rozjaśnił Ci kwestię używania klasycznych referencji i wskaźników surowych w argumentach funkcji.