Start Kontakt

Mechanizm structured binding (wiązanie strukturalne) w C++

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