Menu

C++ pair와 tuple: 구조체 없이 값 묶기

std::pairstd::tuple이 두 개 이상의 값을 하나의 객체로 묶는 방법: 만드는 법, 필드 접근, 구조적 바인딩, 그리고 각각이 어디에 적합한지.

이 페이지에는 실행 가능한 에디터가 있습니다 - 편집하고 실행하면 결과를 바로 볼 수 있습니다.

두 값, 하나의 객체

때로는 두 가지를 함께 묶어 두어야 할 때가 있습니다. 이름과 점수, 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<string, int>아니라 pair<const char*, int>를 추론합니다. 문자열 리터럴은 std::string이 아닙니다. string이 필요하다면 명시적으로 밝히세요. make_pair(string("hi"), 3)처럼 쓰거나 pair 타입을 그대로 적습니다. 그러지 않으면 나중에 예상치 못한 비교나 복사가 생길 수 있습니다.

구조적 바인딩으로 풀어내기

곳곳에서 .first.second를 읽는 것은 금세 읽기 어려워집니다. 이름이 아무것도 알려 주지 않기 때문이죠. C++17의 구조적 바인딩을 사용하면 두 필드에 한 줄로 진짜 이름을 붙일 수 있습니다.

이는 각 요소가 pairmap범위 기반 for로 순회할 때 빛을 발합니다. it->first / it->second 대신 키와 값에 직접 이름을 붙입니다.

다른 컨테이너 요소와 마찬가지로 루프에서는 const auto&를 사용하세요. 각 pair의 복사를 피하고 읽기만 한다는 것을 알려 줍니다. &를 빼면 모든 항목을 복사하게 되며, 큰 map에서는 조용한 성능 버그가 됩니다.

둘로는 부족할 때: tuple

pair는 두 값에서 멈춥니다. 셋 이상이 필요할 때, std::tuple은 같은 발상을 임의의 개수로 확장한 것입니다. 중괄호 초기화나 make_tuple로 만들고, N이 컴파일 타임에 정해지는 인덱스인 std::get<N>으로 읽습니다.

get<> 안의 인덱스는 컴파일 타임에 정해지는 상수여야 합니다. i가 런타임 변수인 get<i>(record)는 컴파일되지 않습니다. tuple의 필드는 서로 다른 타입을 가질 수 있어서, 요소 타입이 런타임이 아니라 컴파일 중에 결정되어야 하기 때문입니다. 런타임 인덱스를 원하게 된다면, 필요한 것은 아마 vector일 것입니다.

구조적 바인딩은 tuple에도 동작하며, 이것이 tuple을 다루는 읽기 좋은 방법입니다.

여러 값 반환하기

이 타입들에 손을 뻗는 일상적인 이유는, 구조체를 따로 만들거나 출력 매개변수를 다루지 않고도 함수에서 둘 이상의 값을 반환하기 위함입니다. 결과를 pairtuple로 묶고 호출 지점에서 풀어냅니다.

세 개 이상의 결과는 같은 방식으로 tuple을 반환하면 됩니다. std::tie도 있습니다. 이는 새 변수를 선언하는 대신 이미 존재하는 변수에 풀어내는 오래된 기법으로, std::ignore로 특정 필드를 무시하고 싶을 때 유용합니다.

다만 멈춰야 할 때도 있습니다. 같은 필드 묶음이 여러 곳에 나타나거나, .second가 점수인지 개수인지 자꾸 헷갈린다면, 이름 있는 멤버를 가진 struct를 정의하세요. pairtuple은 지역적이고 수명이 짧은 묶음에 가장 적합합니다. 데이터가 하나의 표현식보다 오래 살아남는 순간, 이름 있는 필드가 승리합니다.

비교와 정렬

유용한 보너스: pairtuple에는 _사전식(lexicographic)_으로 동작하는 비교 연산자가 내장되어 있습니다. 첫 번째 요소를 비교하고, 첫 요소가 같을 때만 다음 요소로 넘어갑니다. 그래서 이들은 완벽한 정렬 키가 됩니다.

필드의 순서가 중요하다는 점에 주목하세요. age를 먼저 두면 주로 나이로 정렬하고, 동률일 때 이름으로 정렬합니다. 이름을 먼저 두는 순서를 원한다면 tuple의 요소 순서를 바꾸면 됩니다. 바로 이 기본 비교 때문에 pair<priority, item>이 우선순위 큐에서 흔히 쓰이는 관용구입니다.

다음: 반복자(Iterator)

이제 컨테이너 주위에서 .first, .second, it->first, *it가 등장하는 것을 보았습니다. pair 요소를 그것이 속한 map과 실제로 이어 주는 것이 바로 _반복자(iterator)_입니다. 다음 페이지에서는 반복자를 제대로 파헤칩니다. begin()end()가 정말로 무엇을 반환하는지, ++it가 어떻게 컨테이너를 훑는지, 그리고 C++에서 가장 고약한 미정의 동작을 일으키는 반복자 무효화 함정에 대해 다룹니다.

자주 묻는 질문

C++에서 pair와 tuple의 차이는 무엇인가요?

std::pair는 정확히 두 개의 값을 담으며 .first.second로 접근합니다. std::tuple은 고정된 개수의 값(0개, 2개, 3개 이상)을 담으며 std::get<N>(t)로 접근합니다. pair는 본질적으로 더 친근한 멤버 이름을 가진 두 요소짜리 tuple입니다. 필드가 세 개 이상 필요할 때만 tuple을 사용하세요.

C++에서 tuple 요소에는 어떻게 접근하나요?

컴파일 타임에 정해지는 인덱스로 std::get<N>(t)를 사용합니다(예: std::get<0>(t)). C++17부터는 구조적 바인딩으로도 풀어낼 수 있습니다. auto [a, b, c] = t;는 각 요소에 고유한 이름 있는 변수를 부여합니다. 런타임 변수로 tuple을 인덱싱할 수는 없습니다. std::get<i>i는 상수여야 합니다.

C++에서 함수로부터 여러 값을 어떻게 반환하나요?

std::pairstd::tuple을 반환한 다음 호출 지점에서 구조적 바인딩으로 풀어냅니다. auto [ok, value] = parse(text);처럼요. 이는 출력 매개변수보다 깔끔하고 일회용 구조체를 정의할 필요가 없습니다. 다만 필드가 한 번의 호출을 넘어서 살아남는다면 이름 있는 구조체가 더 읽기 좋습니다.

Coddy programming languages illustration

Coddy로 코딩 배우기

시작하기