Dwie wartości, jeden obiekt
Czasem trzeba trzymać razem dwie rzeczy: imię i wynik, x i y, flagę „czy się udało” i rezultat. Można by definiować struct dla każdej takiej pary, ale przy grupowaniach jednorazowych to sporo ceremonii. std::pair (z <utility>) łączy dokładnie dwie wartości w jeden obiekt, a std::tuple (z <tuple>) uogólnia to na dowolną stałą liczbę wartości.
Z std::pair masz już pośrednio do czynienia: każdy element std::map to pair<const Key, Value>. Ta strona pokazuje to wprost i przedstawia nowoczesne, czytelne sposoby budowania i rozpakowywania tych typów.
Dwa pola zawsze nazywają się .first i .second: nie przejmują nazw twoich zmiennych. To cena typu generycznego: nazwy pól mówią o pozycji, a nie o znaczeniu.
Tworzenie par
Są trzy popularne sposoby tworzenia pair i wszystkie dają ten sam obiekt.
make_pair dedukuje za ciebie typy elementów, co było wygodne przed C++17. Dziś inicjalizacja klamrowa z dedukcją argumentów szablonu klasy (pair p{"Boris", 85};) pokrywa większość przypadków, ale make_pair nadal zobaczysz wszędzie w istniejącym kodzie.
Jedna pułapka dedukcji: make_pair("hi", 3) dedukuje pair<const char*, int>, a nie pair<string, int>. Literały tekstowe to nie std::string. Jeśli potrzebujesz string, powiedz to jawnie, czyli make_pair(string("hi"), 3) albo pełny typ pary, bo inaczej później możesz trafić na zaskakujące porównania albo kopie.
Rozpakowywanie przez structured bindings
Czytanie wszędzie .first i .second szybko staje się nieczytelne, bo te nazwy nic nie mówią. Structured bindings z C++17 pozwalają w jednej linii nadać obu polom prawdziwe nazwy:
To błyszczy w pętli for po zakresie po map, gdzie każdy element jest pair. Zamiast it->first / it->second nazywasz klucz i wartość bezpośrednio:
W pętli używaj const auto&, tak jak dla każdego elementu kontenera: unikasz kopiowania każdej pair i sygnalizujesz, że tylko czytasz. Pomiń &, a skopiujesz każdy wpis; przy dużej mapie to cichy błąd wydajności.
Gdy dwie to za mało: tuple
pair kończy się na dwóch wartościach. Gdy potrzebujesz trzech lub więcej, std::tuple to ta sama idea z dowolną liczbą elementów. Tworzysz ją przez inicjalizację klamrową albo make_tuple, a odczytujesz przez std::get<N>, gdzie N to indeks znany w czasie kompilacji.
Indeks w get<> musi być stałą znaną w czasie kompilacji. get<i>(record), gdzie i jest zmienną, się nie skompiluje: pola krotki mogą mieć różne typy, więc typ elementu musi zostać ustalony podczas kompilacji, a nie w czasie działania programu. Jeśli chcesz indeksu znanego dopiero w czasie wykonania, prawdopodobnie potrzebujesz vector.
Structured bindings działają też na krotkach i to jest czytelny sposób ich użycia:
Zwracanie wielu wartości
Codzienny powód, by sięgać po te typy, to zwracanie więcej niż jednej wartości z funkcji bez wymyślania struktury czy żonglowania parametrami wyjściowymi. Spakuj wyniki w pair albo tuple i rozpakuj je w miejscu wywołania.
Przy trzech lub więcej wynikach zwróć tuple w ten sam sposób. Jest też std::tie, starsza sztuczka, która rozpakowuje do istniejących zmiennych zamiast deklarować nowe; przydaje się, gdy chcesz pominąć pole przez std::ignore:
Kiedy jednak przestać: jeśli ta sama grupa pól pojawia się w więcej niż jednym miejscu albo ciągle zapominasz, czy .second to wynik, czy licznik, zdefiniuj struct z nazwanymi polami. pair i tuple najlepiej sprawdzają się przy lokalnych, krótko żyjących grupowaniach; nazwane pola wygrywają, gdy tylko dane żyją dłużej niż jedno wyrażenie.
Porównywanie i sortowanie
Przydatny bonus: pair i tuple mają wbudowane operatory porównania, które działają leksykograficznie: porównują pierwszy element i dopiero przy równości przechodzą do następnego. To czyni je idealnymi kluczami do sortowania.
Zauważ, że kolejność pól ma znaczenie: umieszczenie age jako pierwszego sortuje przede wszystkim po wieku, a imię rozstrzyga remisy. Gdyby sortowanie miało iść najpierw po imieniu, trzeba by zamienić kolejność elementów krotki. To domyślne porównanie jest właśnie powodem, dla którego pair<priority, item> to popularny idiom przy kolejkach priorytetowych.
Dalej: iteratory
Przy kontenerach pojawiają się już .first, .second, it->first i *it: to, co faktycznie łączy element pair z map, w której mieszka, to iterator. Następna strona porządnie omawia iteratory: co naprawdę zwracają begin() i end(), jak ++it przechodzi po kontenerze i jakie pułapki unieważnienia iteratorów powodują jedne z najpaskudniejszych przypadków niezdefiniowanego zachowania w C++.
Najczęściej zadawane pytania
Czym się różni pair od tuple w C++?
std::pair przechowuje dokładnie dwie wartości, dostępne przez .first i .second. std::tuple przechowuje dowolną stałą liczbę wartości (zero, dwie, trzy lub więcej), dostępnych przez std::get<N>(t). pair to w zasadzie dwuelementowa krotka z przyjaźniejszymi nazwami pól; po tuple sięgaj tylko wtedy, gdy potrzebujesz trzech lub więcej pól.
Jak odczytać elementy tuple w C++?
Użyj std::get<N>(t) z indeksem znanym w czasie kompilacji, np. std::get<0>(t). Od C++17 możesz też rozpakować krotkę przez structured bindings: auto [a, b, c] = t; daje każdemu elementowi osobną nazwaną zmienną. Nie możesz indeksować krotki zmienną znaną dopiero w czasie wykonania: std::get<i> wymaga stałego i.
Jak zwrócić wiele wartości z funkcji w C++?
Zwróć std::pair albo std::tuple i rozpakuj wynik w miejscu wywołania przez structured bindings: auto [ok, value] = parse(text);. To czystsze niż parametry wyjściowe i nie wymaga definiowania jednorazowej struktury, choć nazwana struktura jest czytelniejsza, gdy pola żyją dłużej niż jedno wywołanie.