Czy wydaje ci się, że wiesz już wszystko o zwracaniu wartości z funkcji/metody? Istnieje mnóstwo przypadków, w których trzeba nieco pomyśleć zanim napiszemy sygnaturę funkcji. W dzisiejszym artykule poznamy cztery sposoby na zwrócenie wartości z funkcji w pewnym kontrowersyjnym przypadku.

Zadanie

Wyobraź sobie, że jesteś bardzo zaradnym programistą i realizujesz zlecenie warte dużą ilość pieniędzy. Sądzisz, że potrafisz rozwiązać absolutnie każdy problem. Stoi przed tobą nadzwyczaj proste zadanie: pobierz z bazy danych informacje o pracowniku znajdującym się pod specyficznym ID.

Pozornie nie kryje się tutaj nic skomplikowanego ani podchwytliwego, prawda? Ale to tylko złudzenie. Przy realizacji tego prostego zadania czeka na nas cała masa różnorodnych pułapek. Musimy chociażby się zastanowić, co zrobić gdy nie znajdziemy pracownika o podanym ID. Jak rozwiązać ten problem? Co zwrócić w takim wypadku?

I sposób Unix way

#include <iostream>
#include <vector>

using std::string;
using std::vector;
using std::cout;
using std::endl;
struct Worker {
    int ID;
    string name;
    string surname;
};
vector<Worker> workerList {{1,"Jan","Kowalski"},{2,"Adam","Nowak"},{3,"Juliusz", "Słowacki"}}; // symulacja bazy danych

Worker getWorkerByID(int ID) {
    for(Worker worker : workerList) {
        if(worker.ID==ID) return worker;
    }
    return Worker{-1,"",""};
}
int main()
{
    Worker work1 = getWorkerByID(1);
    Worker work2 = getWorkerByID(4);
    if(work1.ID!=-1) cout<<"Pracownik ID 1:"<<work1.name<<" "<<work1.surname<<endl;
    else cout<<"Nie znaleziono pracownika o ID 1"<<endl;
    if(work2.ID!=-1) cout<<"Pracownik ID 2:"<<work2.name<<" "<<work2.surname<<endl;
    else cout<<"Nie znaleziono pracownika o ID 2"<<endl;
}


To jest najprostsze rozwiązanie. W ten sposób do tego problemu podeszłoby 95% studentów. Kod realizuje stawiane przed nim zadanie. Funkcja zwróci nam zawsze pracownika o specyficznym ID, jeśli go znajdzie.

Jednakże, co zrobić w sytuacji, kiedy pracownika o podanym jako argument ID nie ma w bazie? Możemy zwrócić obiekt Worker o specyficznych wartościach atrybutów (tak jak w powyższym kodzie, linijka 19). Ale wymaga to nieintuicyjnego ifa po stronie osoby korzystającej z napisanej przez nas funkcji. Dlaczego nieintuicyjnego? Ponieważ korzystający z naszej funkcji musi wiedzieć lub domyśleć się, co mówi o tym, że zwrócony obiekt nie jest tym czego poszukiwał. Czy ID=-1 może to sugerować? A może puste pole imię/nazwisko? Taki sposób rozwiązania problemu rodzi wiele komplikacji.

Nazwałem ten sposób „Unix way” gdyż jest on powszechnie wykorzystywany w API systemu operacyjnego Linux. Tam większość funkcji w przypadku niepowodzenia zwraca wartość -1. Aby uzyskać dokładny kod błędu, trzeba nierzadko korzystać z dodatkowych funkcji lub odczytywać dodatkowe zmienne (errno)).

II sposób rozwiązania problemu – wskaźniki i null

Wskaźniki – brzmi bardzo nieprzyjemnie. Ale wbrew pozorom jest to pewne rozwiązanie. Spróbujmy przerobić funkcję, tak aby korzystała z ich dobrodziejstwa.

#include <iostream>
#include <vector>

using std::string;
using std::vector;
using std::cout;
using std::endl;
struct Worker {
    int ID;
    string name;
    string surname;
};
vector<Worker> workerList {{1,"Jan","Kowalski"},{2,"Adam","Nowak"},{3,"Juliusz", "Słowacki"}}; // symulacja bazy danych

Worker* getWorkerByID(int ID) {
    for(Worker worker : workerList) {
        if(worker.ID==ID) return new Worker(worker);
    }
    return nullptr;
}
int main()
{
    Worker* work1 = getWorkerByID(1);
    Worker* work2 = getWorkerByID(4);
    if(work1!=nullptr) {
        cout<<"Pracownik ID 1:"<<work1->name<<" "<<work1->surname<<endl;
        delete work1;
    }
    else cout<<"Nie znaleziono pracownika o ID 1"<<endl;
    if(work2!=nullptr) {
        cout<<"Pracownik ID 2:"<<work2->name<<" "<<work2->surname<<endl;
        delete work2;
    }
    else cout<<"Nie znaleziono pracownika o ID 2"<<endl;
}

Dla programisty korzystającego z naszej funkcji jej działanie wydaje się być bardziej oczywiste niż poprzednio. Jeśli pracownik nie został znaleziony – funkcja zwraca null. Problemem jest zarządzanie pamięcią. Korzystający z naszej funkcji musi pamiętać o zwolnieniu pamięci. Moglibyśmy oczywiście skorzystać z dobrodziejstwa nowoczesnego wskaźnika unique_ptr czy shared_ptr. Pominęliśmy to w tym przykładzie, gdyż zaciemniło by kod i nie pozwoliło się skupić na głównym przekazie.

III sposób rozwiązania problemu – wyjątki

A co gdybyśmy zatrudnili do rozwiązania naszej bolączki wyjątki? W Javie są one używane do obsługi praktycznie każdej sytuacji wyjątkowej. Dlaczego nie mielibyśmy zrobić czegoś takiego samego w C++?

#include <iostream>
#include <vector>

using std::string;
using std::vector;
using std::cout;
using std::endl;
struct Worker {
    int ID;
    string name;
    string surname;
};
vector<Worker> workerList {{1,"Jan","Kowalski"},{2,"Adam","Nowak"},{3,"Juliusz", "Słowacki"}}; // symulacja bazy danych
class NotFoundException : public std::exception {
public:
    virtual char const * what() const noexcept override{return "Nie znaleziono obiektu";}
};
Worker getWorkerByID(int ID) {
    for(Worker worker : workerList) {
        if(worker.ID==ID) return worker;
    }
    throw NotFoundException();
}
int main()
{
    try {
        Worker work1 = getWorkerByID(1);
        cout<<"Pracownik ID 1:"<<work1.name<<" "<<work1.surname<<endl;
    }
    catch(NotFoundException e) {
        cout<<e.what();
    }
    try {
        Worker work2 = getWorkerByID(4);
        cout<<"Pracownik ID 2:"<<work2.name<<" "<<work2.surname<<endl;
    }
    catch(NotFoundException e) {
        cout<<e.what();
    }
}

Rozwiązanie jest „prawie” idealne i na pewno jest o wiele lepsze od poprzedniego sposobu.

Zrobiliśmy sobie wyjątek NotFoundException. Wyrzucamy go wtedy, gdy pracownik o danym w sygnaturze funkcji ID nie zostanie odnaleziony. Od funkcji dostajemy zwykłą zmienną, której czasem życia zarządza kompilator. Nie musimy zatem dbać o zarządzanie pamięcią. Ponadto każda z sytuacji – powodzenie i porażka – są jasno rozróżnialne i oznajmiane albo zwróceniem odpowiedniej wartości albo wyrzuceniem wyjątku. Jaka jest wada? Dobrze zrealizowaną obsługę wyjątków w C++ ciężko zaprojektować i zakodować.

Najlepszy sposób – optional

optional jest nowym typem wprowadzonym wraz ze standardem C++17. Czym wyjątkowym się charakteryzuje? Optional jest zwykłą klasą, której obiekt może przechowywać dowolny inny obiekt lub wartość. Ponadto optional wie o tym, czy jest wypełniony jakąś zawartością czy jest pusty. Dzięki temu możemy łatwo sprawdzić, czy funkcja zwróciła rzeczywiście jakieś dane czy nie zwróciła nic. Wystarczy zapytać optionala: „Czy jesteś wypełniony jakąś zawartością”? On nam uprzejmie odpowie, dzięki czemu nasz kod będzie bardziej elegancki.

Jak użyć std::optional?

Jeśli nasz kompilator wspiera standard C++17, dodajemy nagłówek <optional>. Optional znajduje się wtedy w przestrzeni nazw std. Jeśli posiadamy starszą wersję kompilatora gcc (np.: tą dostarczaną z wersją C::B 17.12) powinniśmy dołączyć nagłówek <experimental/optional> Optional znajduje się wtedy w przestrzeni nazw std::experimental.

Przeróbmy analizowany wcześniej przykład tak, aby korzystał z optionali.

#include <iostream>
#include <vector>
#include <experimental/optional>

using std::string;
using std::vector;
using std::cout;
using std::endl;
using std::experimental::optional;
struct Worker {
    int ID;
    string name;
    string surname;
};
vector<Worker> workerList {{1,"Jan","Kowalski"},{2,"Adam","Nowak"},{3,"Juliusz", "Słowacki"}}; // symulacja bazy danych

optional<Worker> getWorkerByID(int ID) {
    optional<Worker> opt;
    for(Worker worker : workerList) {
        if(worker.ID==ID) opt = worker;
    }
    return opt;
}
int main()
{
    optional<Worker> work1 = getWorkerByID(1);
    optional<Worker> work2 = getWorkerByID(4);
    if(work1) cout<<"Pracownik ID 1:"<<work1->name<<" "<<work1->surname<<endl;
    else cout<<"Nie znaleziono pracownika o ID 1"<<endl;
    if(work2) cout<<"Pracownik ID 2:"<<work2->name<<" "<<work2->surname<<endl;
    else cout<<"Nie znaleziono pracownika o ID 2"<<endl;
}

Zauważamy zmianę definicji funkcji w linijce 17. Teraz funkcja zwraca obiekt optional przechowujący obiekty klasy Worker. Pustego optionala tworzymy w linijce 18. Jak wpisujemy coś do optionala? Po prostu stosujemy zwykłe przypisanie. Przykład znajduje się w linijce 20.

Jak sprawdzamy czy optional przechowuje jakąś wartość? Po prostu weryfikujemy, czy obiekt zwraca true czy false. Optional posiada przeciążony operator bool, dzięki temu taki mechanizm działania jest możliwy.

Jeśli chcesz poznać bardziej zaawansowane cechy Optionala, w tym miejscu jest to ciekawie opisane

Podsumowując

Ten artykuł miał za zadanie w krótki sposób przedstawić ci przegląd rozwiązań problemu. Problemu polegającego na obsłudze sytuacji wyjątkowych podczas zwracania wartości. Optional jest najlepszym możliwym rozwiązaniem w sytuacji, kiedy chcemy zwrócić jakiś obiekt lecz istnieją sytuacje kiedy ten obiekt nie będzie istniał. Innym przydatnym sposobem który warto znać są wyjątki. Te dwie metody są dwoma najczęściej obecnie używanymi. Najgorszym sposobem jest pierwszy. Powinniśmy go używać tylko w sytuacji, kiedy absolutnie nie mamy innego wyboru.

Dzięki za lekturę i powodzenia 🙂 Jeśli masz jakieś wątpliwości, uwagi, podziel się swoją opinią 🙂

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *