Два значения, один объект
Иногда нужно держать вместе две вещи — имя и счёт, x и y, флаг «сработало ли?» и результат. Вы могли бы определять struct для каждой такой связки, но для одноразовых группировок это слишком много церемоний. std::pair (из <utility>) объединяет ровно два значения в один объект, а std::tuple (из <tuple>) обобщает это на любое фиксированное число значений.
Со std::pair вы уже косвенно встречались по пути сюда: каждый элемент std::map — это pair<const Key, Value>. Эта страница делает это явным и показывает современные, читаемые способы создавать и распаковывать эти типы.
Эти два члена всегда называются .first и .second — они не берут имена ваших переменных. Это цена обобщённого типа: имена полей позиционные, а не описательные.
Создание pair
Есть три распространённых способа создать pair, и все они порождают один и тот же объект.
make_pair выводит типы элементов за вас, что было удобно до C++17. Сегодня инициализация фигурными скобками плюс вывод аргументов шаблона класса (pair p{"Boris", 85};) покрывает большинство случаев, но make_pair вы по-прежнему будете встречать повсюду в существующем коде.
Одна ловушка с выводом типов: make_pair("hi", 3) выводит pair<const char*, int>, а не pair<string, int>. Строковые литералы — это не std::string. Если вам нужен string, скажите об этом явно — make_pair(string("hi"), 3) или выпишите тип pair полностью, — иначе позже вы можете получить неожиданные сравнения или копии.
Распаковка через structured bindings
Читать .first и .second повсюду быстро становится нечитаемо, потому что имена ничего не говорят. Structured bindings из C++17 позволяют дать двум полям настоящие имена в одну строку:
Это особенно хорошо проявляется в цикле for по диапазону над map, где каждый элемент — это pair. Вместо it->first / it->second вы напрямую называете ключ и значение:
В цикле используйте const auto&, как и для любого элемента контейнера — это позволяет избежать копирования каждого pair и сигнализирует, что вы только читаете. Уберите & — и вы копируете каждый элемент; на большой map это тихая ошибка производительности.
Когда двух мало: tuple
pair останавливается на двух значениях. Когда нужно три и более, std::tuple — та же идея с произвольным числом. Создаёте его инициализацией фигурными скобками или через make_tuple, а читаете через std::get<N>, где N — индекс, известный на этапе компиляции.
Индекс внутри get<> должен быть константой, известной на этапе компиляции. get<i>(record), где i — переменная времени выполнения, не скомпилируется: поля кортежа могут иметь разные типы, поэтому тип элемента должен быть определён во время компиляции, а не во время выполнения. Если вы ловите себя на желании индексировать во время выполнения, вам, вероятно, нужен vector.
Structured bindings работают и с кортежами, и это читаемый способ их использовать:
Возврат нескольких значений
Повседневная причина обращаться к этим типам — вернуть из функции более одного значения, не выдумывая struct и не жонглируя выходными параметрами. Объедините результаты в pair или tuple и распакуйте в месте вызова.
Для трёх и более результатов возвращайте tuple тем же способом. Есть ещё std::tie — более старый приём, который распаковывает в уже существующие переменные, а не объявляет новые, и полезен, когда вы хотите проигнорировать поле через std::ignore:
Однако важно знать, когда остановиться: если одна и та же группа полей встречается более чем в одном месте или вы постоянно забываете, .second — это счёт или количество, определите struct с именованными членами. pair и tuple лучше всего подходят для локальных, недолговечных группировок; именованные поля выигрывают, как только данные живут дольше одного выражения.
Сравнение и сортировка
Удобный бонус: pair и tuple поставляются со встроенными операторами сравнения, которые работают лексикографически — они сравнивают первый элемент и переходят к следующему, только когда первые равны. Это делает их идеальными ключами сортировки.
Обратите внимание, что порядок полей имеет значение: если поставить age первым, сортировка идёт прежде всего по возрасту, а по имени — как по дополнительному критерию. Если бы вы хотели порядок сначала по имени, вы поменяли бы местами порядок элементов кортежа. Именно это сравнение по умолчанию и есть причина, по которой pair<priority, item> — распространённая идиома для очередей с приоритетом.
Далее: Итераторы
Теперь вы видели, как .first, .second, it->first и *it всплывают вокруг контейнеров — то, что на самом деле связывает элемент pair с map, в которой он живёт, — это итератор. Следующая страница разбирает итераторы как следует: что на самом деле возвращают begin() и end(), как ++it проходит по контейнеру и ловушки инвалидации итераторов, которые вызывают одни из самых неприятных случаев неопределённого поведения в C++.
Часто задаваемые вопросы
В чём разница между pair и tuple в C++?
std::pair хранит ровно два значения, доступ к которым осуществляется через .first и .second. std::tuple хранит любое фиксированное число значений (ноль, два, три и более), доступ к которым осуществляется через std::get<N>(t). По сути pair — это кортеж из двух элементов с более удобными именами полей; берите tuple только тогда, когда нужно три поля и более.
Как обращаться к элементам tuple в C++?
Используйте std::get<N>(t) с индексом, известным на этапе компиляции, например std::get<0>(t). Начиная с C++17 можно также распаковать его через structured bindings: auto [a, b, c] = t; даёт каждому элементу собственную именованную переменную. Нельзя индексировать кортеж переменной времени выполнения — std::get<i> требует, чтобы i была константой.
Как вернуть несколько значений из функции в C++?
Верните std::pair или std::tuple и распакуйте его в месте вызова через structured bindings: auto [ok, value] = parse(text);. Это чище, чем выходные параметры, и избавляет от необходимости определять одноразовый struct, хотя именованный struct читается лучше, когда поля живут дольше одного вызова.