Czasami funkcje C++ zwaracają więcej niż jedną wartość. W takich przypadkach chcielibyśmy zwracane wartości przetworzyć w sposób prosty i zrozumiały dla osoby, która będzie kiedyś czytać nasz kod (lub dla nas, już po kilku miesiącach przerwy).
Przykładowe kody prezentowane w tym artykule można pobrać z Google Drive.
Załóżmy, że w języku C++ wykorzystujemy strukturę odpowiadającą osobie, z zapisanymi w niej wartościami takimi jak imię, wzrost, wiek czy informacja o posiadaniu prawa jazdy. Pewna funkcja (setDrivingLicense
) zmienia wartość jednego z pól i zwraca ją razem z wartością pola kluczowego, jaką jest imię.
struct Person
{
std::string name;
unsigned int age;
unsigned int height;
bool drivingLicense;
};
using NameAndLicense = std::pair<std::string, bool>;
NameAndLicense setDrivingLicense(Person person, bool dL)
{
NameAndLicense val;
val.first = person.name;
val.second = dL;
return val;
}
W takim przypadku dwie zwracane wartości możemy opakować typem std::pair
i zwrócić odpowiednią zmienną. Problem powstaje wtedy, gdy chcemy przetwarzać poszczególne składowe zmiennej std::pair
. Chodzi o to, że należy się do nich odwoływać poprzez metody first
i second
, co nie jest zbyt czytelne.
Person jasiek { "Jasiek", 70, 185, true };
NameAndLicense ret;
ret = setDrivingLicense(jasiek, false);
std::cout << std::boolalpha << ret.first << "; " << ret.second << std::endl;
W przypadku typu std::pair
nie jest jeszcze źle — w końcu to tylko 2 wartości, więc można łatwo zapamiętać, która jest pierwsza, a która druga. Jednak za chwilę dowiemy się, że można też zwracać więcej wartości, i co gorsza, w takim przypadku metoda odwoływania się do nich nie jest zbyt elegancka.
Z pomocą przychodzi w takim przypadku rozwiązanie zwane structured binding (wiązanie strukturalne lub wiązanie strukturyzowane), dostępne od wersji języka C++17. Pozwala ono na przypisanie zwracanych wartości do wielu zmiennych jednocześnie. Spójrzmy, jak to działa:
auto [name, hasLicense] = setDrivingLicense(jasiek, false);
std::cout << std::boolalpha << name << "; " << hasLicense << std::endl;
Zamiast tworzyć zmienną typu Person
i przypisywać do niej wartość zwracaną przez funkcję setDrivingLicense()
, deklarujemy 2 zmienne automatyczne name
i hasLicense
, a następnie od razu w jednym wierszu kodu inicjalizujemy je odpowiednimi wartościami zwracanymi. Następnie możemy je już normalnie używać w programie. Nie dość, że kod się upraszcza, to jeszcze staje się bardziej zrozumiały — zawsze pamiętajmy o zasadzie KISS ("keep it simple, stupid", czyli "zrób to prosto, głupku").
A teraz to, o czym wspominaliśmy na początku - funkcja zwracająca więcej niż 2 wartości. Załóżmy, że funkcja będzie ustawiać zarówno informację o prawie jazdy, jak i wiek osoby. Trzy wartości można spakować w zmiennej typu std::tuple
(to uogólnienie std::pair
pozwalające na używanie pseudo-struktur z elementami o różnych typach).
using NameAndLicenseAndAge = std::tuple<std::string, bool, unsigned int>;
NameAndLicenseAndAge setDrivingLicenseAndAge(Person person, bool dL, unsigned int a)
{
NameAndLicenseAndAge val;
std::get<0>(val) = person.name;
std::get<1>(val) = dL;
std::get<2>(val) = a;
return val;
}
Zwróćmy uwagę na to, jak modyfikowane są elementy zmiennej typu std::tuple
. Jest w tym celu używana funkcja std::get
z odpowiednimi parametrami. Nie wygląda to zbyt czytelnie i faktycznie będzie to nieładnie wyglądać w przypadku, gdy spróbujemy przetwarzać otrzymane wartości.
NameAndLicenseAndAge ret2;
ret2 = setDrivingLicenseAndAge(jasiek, false, 71);
std::cout << std::boolalpha << std::get<0>(ret2) << "; " << std::get<1>(ret2) << "; " << std::get<2>(ret2) << std::endl;
Spróbujmy teraz zapisać powyższy kod za pomocą structured binding:
auto [name, hasLicense, age] = setDrivingLicenseAndAge(jasiek, false, 71);
std::cout << std::boolalpha << name << "; " << hasLicense << "; " << age << std::endl;
Kod wygląda prościej i dużo czytelniej.
Mechanizmu structured binding można też używać w pętlach for
, w celu sprawniejszego przetwarzania danych.
Załóżmy, że mamy wektor struktur Person
i w pętli chcemy dla każej osoby odwrócić wartość pola drivingLicense
, a także dodać 1 do pola age
. Można to zrealizować w taki sposób:
std::vector<Person> persons { { "Jasiek", 70, 185, true }, { "Zygmunt", 35, 175, true }, { "Mietek", 39, 183, false } };
for (auto& [name, age, height, hasLicense] : persons)
{
hasLicense = !hasLicense;
age++;
std::cout << std::boolalpha << name << "; " << hasLicense << "; " << age << std::endl;
}
W pętli for
deklarujemy 4 zmienne: name
, age
, height
i hasLicense
. Od razu też wczytujemy do nich odpowiednie wartości z odpowiedniej struktury zapisanej w wektorze persons
. Iteracje pętli trwają aż do przetworzenia wszystkich elementów wektora.
Oto wyniki uzyskane w konsoli:
Jasiek; false; 71
Zygmunt; false; 36
Mietek; true; 40