본문으로 건너뛰기 C++ tuple 상세 가이드 | '튜플' 가이드

C++ tuple 상세 가이드 | '튜플' 가이드

C++ tuple 상세 가이드 | '튜플' 가이드

이 글의 핵심

C++ tuple 상세 가이드: "튜플" 가이드. 기본 사용·structured binding (C++17).

들어가며

std::tuple은 C++11에서 도입된 타입으로, 여러 값을 하나로 묶어 저장합니다. 함수에서 여러 값을 반환하거나, 임시로 여러 값을 그룹화할 때 유용합니다.

#include <tuple>
#include <iostream>
#include <string>

int main() {
    // tuple 생성: 여러 타입의 값을 하나로 묶음
    // <int, double, std::string>: 저장할 타입들 (순서 중요)
    std::tuple<int, double, std::string> t{42, 3.14, "hello"};
    
    // 접근: std::get<인덱스>(tuple)
    // 인덱스는 컴파일 타임 상수여야 함 (템플릿 인자)
    int x = std::get<0>(t);        // 첫 번째 요소 (int)
    double y = std::get<1>(t);     // 두 번째 요소 (double)
    std::string z = std::get<2>(t); // 세 번째 요소 (string)
    
    std::cout << x << ", " << y << ", " << z << std::endl;
    
    return 0;
}

출력:

42, 3.14, hello

1. 기본 사용

std::tuple을 실제로 다루기 시작하면 가장 먼저 마주치는 것이 생성과 접근입니다. 생성 방식은 C++ 버전에 따라 세 가지가 있고, 접근 방식도 인덱스 기반과 타입 기반 두 가지가 있어 상황에 맞게 골라 쓸 수 있습니다.

생성

std::tuple<int, double> t1{42, 3.14}처럼 타입을 명시적으로 적어도 되고, std::make_tuple로 인자로부터 타입을 자동 추론하게 할 수도 있으며, C++17부터는 클래스 템플릿 인자 추론(CTAD) 덕분에 std::tuple t3{...}처럼 make_tuple 없이도 타입을 생략할 수 있습니다. 아래 예제는 이 세 가지 생성 방식을 나란히 보여주는데, make_tuple로 만든 t2에서 문자열 리터럴 "hello"std::string이 아니라 const char*로 추론된다는 점을 눈여겨봐야 합니다. std::tuple_size_v<decltype(t1)>처럼 컴파일 타임에 튜플의 요소 개수를 확인할 수 있다는 것도, 튜플이 런타임 정보 없이도 타입 시스템 안에서 완전히 다뤄질 수 있는 정적인 자료구조임을 보여줍니다.

#include <tuple>
#include <iostream>
#include <string>

int main() {
    // 방법 1: 직접 생성 (타입 명시)
    std::tuple<int, double> t1{42, 3.14};
    
    // 방법 2: make_tuple (타입 자동 추론)
    // "hello"는 const char*로 추론됨
    auto t2 = std::make_tuple(42, 3.14, "hello");
    
    // 방법 3: C++17 CTAD (Class Template Argument Deduction)
    // 타입 명시 없이 생성자 인자로부터 타입 추론
    // std::string("world"): 명시적으로 string 타입 지정
    std::tuple t3{42, 3.14, std::string("world")};  // C++17
    
    // 크기: tuple의 요소 개수를 컴파일 타임에 확인
    // std::tuple_size_v: tuple의 요소 개수 반환 (C++17)
    // decltype(t1): t1의 타입 추출
    constexpr size_t size1 = std::tuple_size_v<decltype(t1)>;  // 2
    constexpr size_t size2 = std::tuple_size_v<decltype(t2)>;  // 3
    
    std::cout << "t1 크기: " << size1 << std::endl;
    std::cout << "t2 크기: " << size2 << std::endl;
    
    return 0;
}

출력:

t1 크기: 2
t2 크기: 3

접근

튜플의 요소는 [] 연산자가 아니라 std::get<N>(t)처럼 템플릿 인자로 인덱스를 받는 함수로 접근합니다. 배열과 달리 인덱스가 반드시 컴파일 타임 상수여야 하는데, 이는 튜플이 이종(heterogeneous) 타입을 담기 때문에 각 위치마다 반환 타입이 다르고, 컴파일러가 타입을 결정하려면 인덱스를 컴파일 시점에 알아야 하기 때문입니다. 각 타입이 튜플 안에서 유일하다면 std::get<int>(t)처럼 타입 자체로도 접근할 수 있어, 인덱스보다 의도가 드러나는 코드를 작성할 수 있습니다.

#include <tuple>
#include <iostream>
#include <string>

int main() {
    std::tuple<int, double, std::string> t{42, 3.14, "hello"};
    
    // 인덱스로 접근
    int x = std::get<0>(t);
    double y = std::get<1>(t);
    std::string z = std::get<2>(t);
    
    std::cout << "x: " << x << std::endl;
    std::cout << "y: " << y << std::endl;
    std::cout << "z: " << z << std::endl;
    
    // 타입으로 접근 (타입이 유일할 때)
    int x2 = std::get<int>(t);
    double y2 = std::get<double>(t);
    std::string z2 = std::get<std::string>(t);
    
    std::cout << "x2: " << x2 << std::endl;
    
    return 0;
}

출력:

x: 42
y: 3.14
z: hello
x2: 42

2. structured binding (C++17)

std::get<N>으로 하나씩 꺼내는 방식은 튜플의 요소 개수가 늘어날수록 코드를 읽기 번거롭게 만듭니다. C++17의 구조적 바인딩(structured binding)은 auto [a, b, c] = t;처럼 튜플의 모든 요소를 한 번에 의미 있는 이름의 변수로 분해해줍니다. 아래 parseUser 함수가 반환하는 tuple<int, string, bool>auto [id, name, active]로 즉시 풀어내는 것처럼, 함수가 여러 값을 반환하는 상황에서 구조적 바인딩과 튜플을 함께 쓰면 마치 여러 개의 반환값을 직접 지원하는 것처럼 자연스러운 코드를 작성할 수 있습니다.

#include <tuple>
#include <iostream>
#include <string>

std::tuple<int, std::string, bool> parseUser(const std::string& data) {
    // 파싱 로직 (예시)
    int id = 123;
    std::string name = "Alice";
    bool active = true;
    
    // tuple 반환: 여러 값을 한 번에 반환
    // {id, name, active}: 중괄호 초기화로 tuple 생성
    return {id, name, active};
}

int main() {
    // C++17 structured binding: tuple을 개별 변수로 분해
    // auto: 타입 자동 추론 (tuple<int, string, bool>)
    // [id, name, active]: 각 요소를 받을 변수 이름
    // 순서대로 tuple의 요소가 할당됨
    auto [id, name, active] = parseUser("data");
    
    std::cout << "ID: " << id << std::endl;
    std::cout << "이름: " << name << std::endl;
    std::cout << "활성: " << (active ? "true" : "false") << std::endl;
    
    return 0;
}

출력:

ID: 123
이름: Alice
활성: true

3. tie

구조적 바인딩이 항상 새 변수를 만드는 반면, std::tie는 이미 존재하는 변수들의 레퍼런스를 튜플로 묶어주는 함수입니다. 두 방식은 비슷해 보이지만 “새 변수를 선언하며 값을 받을 것인가, 기존 변수를 재사용할 것인가”라는 뚜렷하게 다른 상황에 대응합니다.

기존 변수에 언팩

std::tie(x, y) = getValues();는 이미 선언되어 있던 x, y에 함수의 반환값을 각각 대입합니다. 구조적 바인딩과 달리 새 변수를 만들지 않고 기존 변수를 그대로 갱신할 수 있어, 반복문 안에서 매번 같은 변수에 값을 채워 넣어야 하는 상황에 적합합니다. 특정 요소는 필요 없다면 std::tie(x, std::ignore) = getValues();처럼 std::ignore를 자리 표시자로 넣어 해당 값을 그냥 버릴 수도 있습니다.

#include <tuple>
#include <iostream>

std::tuple<int, double> getValues() {
    return {42, 3.14};
}

int main() {
    // 기존 변수 선언 (초기화 안 됨)
    int x;
    double y;
    
    // std::tie: 기존 변수들을 참조로 묶어 tuple 생성
    // getValues()의 반환값이 x, y에 각각 할당됨
    // structured binding과 달리 기존 변수를 재사용
    std::tie(x, y) = getValues();
    
    std::cout << "x: " << x << std::endl;
    std::cout << "y: " << y << std::endl;
    
    // std::ignore: 특정 요소를 무시하고 싶을 때 사용
    // 두 번째 요소(double)는 버리고 첫 번째만 받음
    std::tie(x, std::ignore) = getValues();
    std::cout << "x (업데이트): " << x << std::endl;
    
    return 0;
}

출력:

x: 42
y: 3.14
x (업데이트): 42

비교 연산

std::tie의 또 다른 유용한 쓰임새는 클래스에 사전식(lexicographic) 비교 연산자를 손쉽게 구현하는 것입니다. 직접 operator<를 작성하려면 멤버 하나하나를 순서대로 비교하는 지루한 코드를 써야 하지만, 아래 Person::asTuple()처럼 멤버들을 std::tie로 묶어 반환하면 튜플이 이미 제공하는 사전식 비교 연산을 그대로 재사용할 수 있습니다. name이 같으면 age를 비교하고, age도 같으면 height를 비교하는 식의 다단계 비교 로직이 asTuple() < other.asTuple() 한 줄로 표현되는 것이 이 패턴의 핵심 장점입니다.

#include <tuple>
#include <iostream>
#include <string>

struct Person {
    std::string name;
    int age;
    double height;
    
    // 튜플로 비교
    auto asTuple() const {
        return std::tie(name, age, height);
    }
    
    bool operator<(const Person& other) const {
        return asTuple() < other.asTuple();
    }
    
    bool operator==(const Person& other) const {
        return asTuple() == other.asTuple();
    }
};

int main() {
    Person p1{"Alice", 25, 165.5};
    Person p2{"Bob", 30, 175.0};
    Person p3{"Alice", 25, 165.5};
    
    if (p1 < p2) {
        std::cout << "p1 < p2" << std::endl;
    }
    
    if (p1 == p3) {
        std::cout << "p1 == p3" << std::endl;
    }
    
    return 0;
}

출력:

p1 < p2
p1 == p3

4. 실전 예제

지금까지 배운 생성, 접근, 구조적 바인딩, tie를 실제 문제에 적용하는 세 가지 예제를 살펴보겠습니다. 수식 풀이, 컨테이너 정렬, 맵의 복합 키라는 서로 다른 맥락에서 튜플이 어떻게 코드를 단순하게 만드는지 확인할 수 있습니다.

예제 1: 여러 값 반환

이차방정식의 해를 구하는 solveQuadratic은 두 근(x1, x2)과 판별식(discriminant)이라는 세 가지 값을 한 번에 반환해야 하는 전형적인 상황입니다. 실근이 없을 때는 NAN으로 채워 반환하고, 호출부에서는 std::isnan(x1)으로 이를 판별해 실근 존재 여부와 실제 근을 하나의 반환값으로 자연스럽게 표현합니다. 별도의 구조체를 정의하지 않고도 이렇게 관련된 여러 값을 즉석에서 묶어 반환할 수 있다는 점이 튜플의 실용적인 강점입니다.

#include <tuple>
#include <iostream>
#include <string>
#include <cmath>

std::tuple<double, double, double> solveQuadratic(double a, double b, double c) {
    double discriminant = b * b - 4 * a * c;
    
    if (discriminant < 0) {
        return {NAN, NAN, discriminant};
    }
    
    double sqrtD = std::sqrt(discriminant);
    double x1 = (-b + sqrtD) / (2 * a);
    double x2 = (-b - sqrtD) / (2 * a);
    
    return {x1, x2, discriminant};
}

int main() {
    auto [x1, x2, discriminant] = solveQuadratic(1, -5, 6);
    
    if (std::isnan(x1)) {
        std::cout << "실근 없음 (판별식: " << discriminant << ")" << std::endl;
    } else {
        std::cout << "x1: " << x1 << std::endl;
        std::cout << "x2: " << x2 << std::endl;
        std::cout << "판별식: " << discriminant << std::endl;
    }
    
    return 0;
}

출력:

x1: 3
x2: 2
판별식: 1

예제 2: 컨테이너 정렬

여러 필드를 가진 데이터를 다룰 때 각 레코드를 별도 구조체 대신 튜플로 표현해두면, std::get<N>으로 특정 필드를 뽑아 정렬 기준을 자유롭게 조합할 수 있습니다. 아래 예제는 학생 정보를 tuple<int, string, double>(학번, 이름, 점수)로 저장한 벡터를 먼저 점수 기준으로 내림차순 정렬하고, 이어서 점수가 같으면 이름 기준으로 다시 정렬해 동점자 처리까지 보여줍니다. 다중 조건 정렬 로직이 비교 람다 안에 명시적으로 드러나 있어, 정렬 기준이 바뀌어야 할 때 그 부분만 수정하면 된다는 점도 유지보수 관점에서 유용합니다.

#include <tuple>
#include <vector>
#include <algorithm>
#include <iostream>
#include <string>

int main() {
    std::vector<std::tuple<int, std::string, double>> students = {
        {1, "Alice", 85.5},
        {2, "Bob", 92.0},
        {3, "Charlie", 78.5},
        {4, "David", 92.0},
        {5, "Eve", 85.5}
    };
    
    // 점수로 내림차순 정렬
    std::sort(students.begin(), students.end(), 
         {
            return std::get<2>(a) > std::get<2>(b);
        });
    
    std::cout << "=== 점수순 ===" << std::endl;
    for (const auto& [id, name, score] : students) {
        std::cout << name << ": " << score << "점" << std::endl;
    }
    
    // 점수, 이름으로 정렬
    std::sort(students.begin(), students.end(), 
         {
            auto score_a = std::get<2>(a);
            auto score_b = std::get<2>(b);
            if (score_a != score_b) {
                return score_a > score_b;
            }
            return std::get<1>(a) < std::get<1>(b);
        });
    
    std::cout << "\n=== 점수, 이름순 ===" << std::endl;
    for (const auto& [id, name, score] : students) {
        std::cout << name << ": " << score << "점" << std::endl;
    }
    
    return 0;
}

두 정렬을 각각 실행하면 다음과 같은 결과를 얻습니다.

=== 점수순 ===
Bob: 92점
David: 92점
Alice: 85.5점
Eve: 85.5점
Charlie: 78.5점

=== 점수, 이름순 ===
Bob: 92점
David: 92점
Alice: 85.5점
Eve: 85.5점
Charlie: 78.5점

예제 3: 맵 키

std::tuple은 기본적으로 사전식 비교 연산을 제공하기 때문에, std::map이나 std::set처럼 정렬된 연관 컨테이너의 키로도 그대로 사용할 수 있습니다. 아래 예제는 (학번, 과목명)이라는 두 값의 조합을 하나의 복합 키로 삼아 점수를 저장하는데, 별도의 키 클래스와 비교 연산자를 정의하지 않아도 std::map<std::tuple<int, std::string>, double>이라고 선언하는 것만으로 두 필드를 함께 고려하는 키가 완성됩니다. scores.find(key)로 조회한 뒤 반환된 std::pair<key, value>를 구조적 바인딩으로 다시 풀어내는 부분도, 튜플과 구조적 바인딩이 얼마나 자연스럽게 맞물려 동작하는지를 잘 보여줍니다.

#include <tuple>
#include <map>
#include <iostream>
#include <string>

int main() {
    // 복합 키
    std::map<std::tuple<int, std::string>, double> scores;
    
    scores[{1, "Math"}] = 85.5;
    scores[{1, "English"}] = 92.0;
    scores[{2, "Math"}] = 78.5;
    scores[{2, "English"}] = 88.0;
    
    // 조회
    auto key = std::make_tuple(1, "Math");
    if (auto it = scores.find(key); it != scores.end()) {
        auto [student_subject, score] = *it;
        auto [id, subject] = student_subject;
        std::cout << "학생 " << id << ", " << subject << ": " << score << "점" << std::endl;
    }
    
    // 순회
    std::cout << "\n=== 전체 점수 ===" << std::endl;
    for (const auto& [key, score] : scores) {
        auto [id, subject] = key;
        std::cout << "학생 " << id << ", " << subject << ": " << score << "점" << std::endl;
    }
    
    return 0;
}

위 코드를 실행하면 조회한 단일 항목과 전체 순회 결과가 다음처럼 출력됩니다.

학생 1, Math: 85.5점

=== 전체 점수 ===
학생 1, English: 92점
학생 1, Math: 85.5점
학생 2, English: 88점
학생 2, Math: 78.5점

5. tuple 연산

튜플은 요소를 담아두는 역할 외에도, 비교와 연결(concatenation)이라는 두 가지 연산을 표준으로 제공합니다. 두 연산 모두 튜플을 값처럼 다루는 코드를 작성할 때 자주 활용됩니다.

비교

std::tuple==, < 연산자는 각 위치의 요소를 순서대로 비교하는 사전식 비교를 자동으로 구현해줍니다. 아래 예제에서 t1t3는 첫 번째 요소(42)가 같지만 두 번째 요소(3.14 vs 2.71)가 다르므로, 두 번째 요소의 대소 관계가 전체 비교 결과를 결정해 t3 < t1이 참이 됩니다. 이 동작 방식 덕분에 3절에서 본 것처럼 std::tie로 여러 멤버를 묶어 클래스 비교 연산자를 간단히 구현할 수 있는 것입니다.

#include <tuple>
#include <iostream>

int main() {
    std::tuple<int, double> t1{42, 3.14};
    std::tuple<int, double> t2{42, 3.14};
    std::tuple<int, double> t3{42, 2.71};
    
    // 비교
    std::cout << "t1 == t2: " << (t1 == t2) << std::endl;  // 1
    std::cout << "t1 == t3: " << (t1 == t3) << std::endl;  // 0
    std::cout << "t1 < t3: " << (t1 < t3) << std::endl;    // 0
    std::cout << "t3 < t1: " << (t3 < t1) << std::endl;    // 1
    
    return 0;
}

출력:

t1 == t2: 1
t1 == t3: 0
t1 < t3: 0
t3 < t1: 1

연결 (tuple_cat)

두 개 이상의 튜플을 하나로 합쳐야 할 때는 std::tuple_cat을 사용합니다. 아래 예제는 tuple<int, double>tuple<string, bool>을 이어붙여 tuple<int, double, string, bool>이라는 4개 요소짜리 튜플을 만드는데, 이런 연결 기능은 여러 함수에서 각각 반환한 부분 결과들을 하나의 튜플로 합쳐야 하는 상황이나, 가변 인자 템플릿에서 매개변수 팩을 다른 튜플과 결합해야 하는 메타프로그래밍 코드에서 특히 유용합니다.

#include <tuple>
#include <iostream>
#include <string>

int main() {
    std::tuple<int, double> t1{42, 3.14};
    std::tuple<std::string, bool> t2{"hello", true};
    
    // 연결
    auto t3 = std::tuple_cat(t1, t2);
    // std::tuple<int, double, std::string, bool>
    
    auto [x, y, z, w] = t3;
    std::cout << x << ", " << y << ", " << z << ", " << w << std::endl;
    
    return 0;
}

출력:

42, 3.14, hello, 1

6. 자주 발생하는 문제

튜플을 처음 다루는 개발자가 특히 자주 마주치는 세 가지 함정이 있습니다. 런타임 인덱스 접근 시도, 값 복사와 레퍼런스의 혼동, 그리고 구조체 대비 상대적으로 큰 메모리 오버헤드입니다.

문제 1: 인덱스

std::get<N>N은 반드시 컴파일 타임 상수여야 하므로, int i = 0; std::get<i>(t)처럼 런타임 변수를 인덱스로 넘기면 컴파일이 실패합니다. 이는 튜플의 각 요소가 서로 다른 타입일 수 있어, 컴파일러가 std::get의 반환 타입을 결정하려면 인덱스를 컴파일 시점에 알아야 하기 때문입니다. 인덱스를 런타임에 계산해야 하는 상황이라면 튜플 대신 같은 타입의 요소만 담는 std::arraystd::vector를 사용하는 것이 더 적절합니다.

#include <tuple>
#include <iostream>

int main() {
    std::tuple<int, double, std::string> t{42, 3.14, "hello"};
    
    // ❌ 런타임 인덱스
    // int i = 0;
    // auto x = std::get<i>(t);  // 에러: i는 constexpr이 아님
    
    // ✅ 컴파일 타임 인덱스
    auto x = std::get<0>(t);
    std::cout << "x: " << x << std::endl;
    
    return 0;
}

문제 2: 참조

std::make_tuple은 인자를 값으로 복사해 새 튜플을 만들기 때문에, 이렇게 만든 튜플의 요소를 수정해도 원본 변수에는 아무 영향이 없습니다. 반대로 std::tie는 인자로 넘긴 변수들의 레퍼런스를 담은 튜플을 만들기 때문에, 그 튜플을 통해 값을 바꾸면 원본 변수도 함께 바뀝니다. 우측값(rvalue)까지 레퍼런스로 담아야 한다면 std::forward_as_tuple을 사용하는데, 이는 완벽 전달(perfect forwarding)이 필요한 제네릭 코드에서 값의 종류(좌측값/우측값)를 유지한 채로 튜플에 담아야 할 때 유용합니다.

#include <tuple>
#include <iostream>

int main() {
    int x = 42;
    double y = 3.14;
    
    // ❌ 복사
    auto t1 = std::make_tuple(x, y);
    std::get<0>(t1) = 100;
    std::cout << "x: " << x << std::endl;  // 42 (변경 안 됨)
    
    // ✅ 참조
    auto t2 = std::tie(x, y);
    std::get<0>(t2) = 100;
    std::cout << "x: " << x << std::endl;  // 100 (변경됨)
    
    // ✅ forward_as_tuple
    auto t3 = std::forward_as_tuple(x, y);
    std::get<0>(t3) = 200;
    std::cout << "x: " << x << std::endl;  // 200
    
    return 0;
}

출력:

x: 42
x: 100
x: 200

문제 3: 크기

간단한 2개 요소짜리 튜플은 동등한 구조체와 크기가 같을 수 있지만, 요소가 늘어나고 타입이 다양해질수록 정렬(alignment) 요구사항 때문에 패딩이 끼어들어 예상보다 큰 메모리를 차지하는 경우가 생깁니다. 아래 예제에서 tuple<int, double, std::string>이 48바이트나 되는 것은 int, double, std::string이 각각 다른 정렬 요구사항을 가지고 있어 컴파일러가 그 사이에 패딩 바이트를 채워 넣기 때문입니다. 성능이 민감하고 튜플을 대량으로 생성해야 하는 코드라면, 이름 있는 구조체로 필드 순서를 정렬 크기 순으로 재배치해 패딩을 줄이는 편이 메모리 효율에 유리할 수 있습니다.

#include <tuple>
#include <iostream>

struct Point {
    int x, y;
};

int main() {
    // tuple은 오버헤드 있음
    std::tuple<int, int> t;
    std::cout << "tuple 크기: " << sizeof(t) << std::endl;  // 8
    
    // struct는 오버헤드 없음
    Point p;
    std::cout << "struct 크기: " << sizeof(p) << std::endl;  // 8
    
    // 복잡한 tuple
    std::tuple<int, double, std::string> t2;
    std::cout << "복잡한 tuple 크기: " << sizeof(t2) << std::endl;  // 48
    
    return 0;
}

출력:

tuple 크기: 8
struct 크기: 8
복잡한 tuple 크기: 48

7. pair vs tuple

std::pairstd::tuple은 여러 값을 묶는다는 점에서 비슷해 보이지만, pair는 정확히 2개의 값만 담도록 설계되어 있고 .first, .second라는 이름 있는 멤버로 접근한다는 차이가 있습니다. 이 차이 때문에 딱 2개의 값만 다루는 상황에서는 pairstd::get<0>, std::get<1>보다 더 읽기 쉬운 코드를 만들어줍니다.

비교

아래 예제는 같은 (int, double) 데이터를 pairtuple로 각각 표현해 접근 방식을 나란히 비교합니다. pair.first, .second라는 고정된 이름으로 접근해 코드만 봐도 의미를 짐작하기 쉬운 반면, tuplestd::get<0>, std::get<1>처럼 인덱스로 접근해야 해서 요소가 늘어날수록 몇 번째가 무엇을 의미하는지 헷갈리기 쉽습니다.

#include <tuple>
#include <utility>
#include <iostream>

int main() {
    // pair: 2개
    std::pair<int, double> p{42, 3.14};
    std::cout << "pair: " << p.first << ", " << p.second << std::endl;
    
    // tuple: 여러 개
    std::tuple<int, double, std::string> t{42, 3.14, "hello"};
    std::cout << "tuple: " << std::get<0>(t) << ", " 
              << std::get<1>(t) << ", " 
              << std::get<2>(t) << std::endl;
    
    return 0;
}

출력:

pair: 42, 3.14
tuple: 42, 3.14, hello

선택 가이드

특징pairtuple
요소 개수2개여러 개
접근.first, .secondstd::get<N>
가독성높음낮음
용도키-값, 좌표여러 값 반환

이 표를 실제 코드 선택 기준으로 정리하면 다음과 같습니다. 값이 정확히 2개면 pair, 3개 이상이지만 이름을 붙일 만큼 자주 재사용되지 않는 임시 묶음이면 tuple, 필드가 고정적이고 여러 곳에서 이름으로 의미를 명확히 해야 한다면 이름 있는 struct를 쓰는 것이 가독성과 유지보수성 측면에서 가장 안전한 선택입니다.

// ✅ pair: 2개일 때
std::pair<int, std::string> getUserInfo() {
    return {123, "Alice"};
}

// ✅ tuple: 3개 이상
std::tuple<int, std::string, int, double> getUserDetails() {
    return {123, "Alice", 25, 165.5};
}

// ✅ struct: 이름 있는 필드 (권장)
struct User {
    int id;
    std::string name;
    int age;
    double height;
};

8. 실전 예제: 데이터베이스 결과

데이터베이스 쿼리 결과처럼 각 행이 여러 컬럼으로 구성된 데이터를 다룰 때, 매번 전용 구조체를 정의하기보다 using Row = std::tuple<int, std::string, int, double>;처럼 타입 별칭을 붙인 튜플로 표현하면 코드를 빠르게 작성할 수 있습니다. 아래 예제에서 printResultscalculateAverage는 모두 같은 Row 타입을 구조적 바인딩으로 분해해 사용하는데, 컬럼 구성이 바뀌면 using 선언 한 줄과 구조적 바인딩의 변수 목록만 수정하면 되어 여러 함수에 걸친 변경 비용이 낮다는 것이 이 패턴의 실용적인 장점입니다. 다만 컬럼 수가 많아지고 여러 곳에서 재사용된다면, 결국은 이름 있는 구조체로 옮기는 편이 장기적인 가독성에는 더 유리합니다.

#include <tuple>
#include <vector>
#include <iostream>
#include <string>

using Row = std::tuple<int, std::string, int, double>;

std::vector<Row> queryDatabase() {
    return {
        {1, "Alice", 25, 85.5},
        {2, "Bob", 30, 92.0},
        {3, "Charlie", 28, 78.5}
    };
}

void printResults(const std::vector<Row>& results) {
    std::cout << "ID\tName\tAge\tScore" << std::endl;
    std::cout << "---\t----\t---\t-----" << std::endl;
    
    for (const auto& [id, name, age, score] : results) {
        std::cout << id << "\t" << name << "\t" << age << "\t" << score << std::endl;
    }
}

double calculateAverage(const std::vector<Row>& results) {
    double total = 0;
    for (const auto& [id, name, age, score] : results) {
        total += score;
    }
    return total / results.size();
}

int main() {
    auto results = queryDatabase();
    
    printResults(results);
    
    std::cout << "\n평균 점수: " << calculateAverage(results) << std::endl;
    
    return 0;
}

main 함수를 실행하면 학생별 성적표와 평균 점수가 다음처럼 출력됩니다.

ID	Name	Age	Score
---	----	---	-----
1	Alice	25	85.5
2	Bob	30	92
3	Charlie	28	78.5

평균 점수: 85.3333

정리

핵심 요약

  1. tuple: 여러 값 묶기 (C++11)
  2. 접근: std::get<N>, structured binding
  3. tie: 기존 변수에 언팩
  4. 비교: 사전식 비교
  5. 실무: 여러 값 반환, 임시 그룹화

pair vs tuple

특징pairtuple
요소 개수2개여러 개
접근.first, .secondstd::get<N>
가독성높음낮음
크기작음
용도키-값, 좌표여러 값 반환

실전 팁

사용 원칙:

  • 여러 값 반환
  • 임시 그룹화
  • 비교 연산
  • 맵 복합 키

성능:

  • 스택 할당
  • 오버헤드 있음 (패딩)
  • 작은 경우 struct 권장
  • 컴파일 타임 크기

주의사항:

  • 인덱스는 constexpr
  • 참조는 tie/forward_as_tuple
  • 가독성 (struct 고려)
  • 크기 오버헤드

다음 단계


같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.

관련 글


자주 묻는 질문 (FAQ)

Q. 이 내용을 실무에서 언제 쓰나요?

A. Everything about C++ tuple 상세 가이드 : from basic concepts to practical applications.

Q. 선행으로 읽으면 좋은 글은?

A. 각 글 하단의 이전 글 또는 관련 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.

Q. 더 깊이 공부하려면?

A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.


이 글에서 다루는 키워드 (관련 검색어)

C++, tuple, pair, structured-binding, C++11 등으로 검색하시면 이 글이 도움이 됩니다.