C++ Structured Binding | "구조적 바인딩" C++17 가이드

C++ Structured Binding | "구조적 바인딩" C++17 가이드

이 글의 핵심

C++ Structured Binding에 대한 실전 가이드입니다.

들어가며

Structured Binding (구조적 바인딩)은 C++17에서 도입된 기능으로, 튜플, 배열, 구조체 등을 여러 변수로 한 번에 분해할 수 있습니다.

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

int main() {
    // tuple 생성: 여러 타입의 값을 하나로 묶음
    std::tuple<int, double, std::string> t = {42, 3.14, "Hello"};
    
    // 구조적 바인딩 (Structured Binding, C++17)
    // auto [i, d, s]: tuple의 각 요소를 개별 변수로 분해
    // i, d, s는 각각 tuple의 첫 번째, 두 번째, 세 번째 요소
    // 타입은 자동 추론: i는 int, d는 double, s는 string
    auto [i, d, s] = t;
    
    std::cout << i << std::endl;  // 42
    std::cout << d << std::endl;  // 3.14
    std::cout << s << std::endl;  // Hello
}

왜 필요한가?:

  • 간결성: 여러 변수를 한 줄로 선언
  • 가독성: 의미 있는 이름 부여
  • 안전성: 타입 추론으로 실수 방지
  • 편의성: 맵 순회, 함수 반환값 처리
// ❌ 기존 방식: 복잡
std::map<std::string, int> m;
for (const auto& pair : m) {
    std::cout << pair.first << ": " << pair.second << '\n';
}

// ✅ 구조적 바인딩: 간결
for (const auto& [key, value] : m) {
    std::cout << key << ": " << value << '\n';
}

1. 기본 사용

배열

#include <iostream>

int main() {
    int arr[] = {1, 2, 3};
    
    auto [a, b, c] = arr;
    
    std::cout << a << std::endl;  // 1
    std::cout << b << std::endl;  // 2
    std::cout << c << std::endl;  // 3
    
    return 0;
}

구조체

#include <iostream>

struct Point {
    int x;
    int y;
};

int main() {
    Point p = {10, 20};
    
    auto [x, y] = p;
    
    std::cout << "x: " << x << std::endl;  // 10
    std::cout << "y: " << y << std::endl;  // 20
    
    return 0;
}

튜플

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

std::tuple<int, double, std::string> getData() {
    return {42, 3.14, "Hello"};
}

int main() {
    auto [i, d, s] = getData();
    
    std::cout << "int: " << i << std::endl;
    std::cout << "double: " << d << std::endl;
    std::cout << "string: " << s << std::endl;
    
    return 0;
}

2. 참조와 const

복사 vs 참조

#include <iostream>

struct Point {
    int x, y;
};

int main() {
    Point p = {10, 20};
    
    // 방법 1: 복사 (auto)
    // x1, y1은 p.x, p.y의 복사본
    // x1, y1 변경해도 p는 영향 없음
    auto [x1, y1] = p;
    x1 = 100;  // x1만 변경 (p.x는 여전히 10)
    std::cout << "p.x: " << p.x << std::endl;  // 10
    
    // 방법 2: 참조 (auto&)
    // x2, y2는 p.x, p.y의 참조 (별칭)
    // x2, y2 변경하면 p도 변경됨
    auto& [x2, y2] = p;
    x2 = 100;  // p.x가 100으로 변경됨
    std::cout << "p.x: " << p.x << std::endl;  // 100
    
    // 방법 3: const 참조 (const auto&)
    // x3, y3는 p.x, p.y의 const 참조
    // 읽기만 가능, 수정 불가 (불필요한 복사 방지)
    const auto& [x3, y3] = p;
    // x3 = 200;  // ❌ 컴파일 에러: const 참조는 수정 불가
    std::cout << "x3: " << x3 << std::endl;  // 100
    
    return 0;
}

출력:

p.x: 10
p.x: 100
x3: 100

3. 실전 예제

예제 1: 맵 순회

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

int main() {
    std::map<std::string, int> scores = {
        {"Alice", 90},
        {"Bob", 85},
        {"Charlie", 95}
    };
    
    std::cout << "=== 구조적 바인딩 ===" << std::endl;
    // const auto& [name, score]: map의 각 요소(pair)를 분해
    // name: pair.first (키)
    // score: pair.second (값)
    // const auto&: 복사 없이 const 참조로 접근 (효율적)
    for (const auto& [name, score] : scores) {
        // pair.first, pair.second 대신 의미 있는 이름 사용
        std::cout << name << ": " << score << std::endl;
    }
    
    std::cout << "\n=== 기존 방식 ===" << std::endl;
    // 기존 방식: pair 객체로 접근
    // pair.first, pair.second는 의미가 불명확
    for (const auto& pair : scores) {
        std::cout << pair.first << ": " << pair.second << std::endl;
    }
    
    return 0;
}

출력:

=== 구조적 바인딩 ===
Alice: 90
Bob: 85
Charlie: 95

=== 기존 방식 ===
Alice: 90
Bob: 85
Charlie: 95

예제 2: 함수 반환값

#include <tuple>
#include <iostream>

std::tuple<int, int, int> getRGB() {
    return {255, 128, 64};
}

std::pair<int, int> divmod(int dividend, int divisor) {
    return {dividend / divisor, dividend % divisor};
}

int main() {
    auto [r, g, b] = getRGB();
    std::cout << "R: " << r << std::endl;
    std::cout << "G: " << g << std::endl;
    std::cout << "B: " << b << std::endl;
    
    auto [quotient, remainder] = divmod(17, 5);
    std::cout << "17 / 5 = " << quotient << " ... " << remainder << std::endl;
    
    return 0;
}

출력:

R: 255
G: 128
B: 64
17 / 5 = 3 ... 2

예제 3: pair 언팩

#include <algorithm>
#include <vector>
#include <iostream>

int main() {
    std::vector<int> v = {3, 1, 4, 1, 5, 9, 2, 6};
    
    auto [minIt, maxIt] = std::minmax_element(v.begin(), v.end());
    
    std::cout << "최소: " << *minIt << std::endl;  // 1
    std::cout << "최대: " << *maxIt << std::endl;  // 9
    
    // 삽입 결과
    std::map<std::string, int> m;
    auto [it, inserted] = m.insert({"key", 10});
    
    if (inserted) {
        std::cout << "삽입 성공: " << it->first << " = " << it->second << std::endl;
    }
    
    return 0;
}

출력:

최소: 1
최대: 9
삽입 성공: key = 10

예제 4: 구조체 분해

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

struct Person {
    std::string name;
    int age;
    double height;
};

int main() {
    std::vector<Person> people = {
        {"Alice", 25, 165.5},
        {"Bob", 30, 175.0},
        {"Charlie", 28, 180.5}
    };
    
    for (const auto& [name, age, height] : people) {
        std::cout << name << " (" << age << "세, " << height << "cm)" << std::endl;
    }
    
    return 0;
}

출력:

Alice (25세, 165.5cm)
Bob (30세, 175cm)
Charlie (28세, 180.5cm)

4. 자주 발생하는 문제

문제 1: 요소 개수 불일치

#include <tuple>

int main() {
    std::tuple<int, int, int> t = {1, 2, 3};
    
    // ❌ 에러: 3개인데 2개만
    // auto [a, b] = t;
    
    // ✅ 개수 일치
    auto [a, b, c] = t;
    
    return 0;
}

문제 2: 참조 수명

#include <iostream>

struct Point {
    int x, y;
};

int main() {
    // ❌ 댕글링 참조
    // auto& [x, y] = Point{10, 20};  // 임시 객체
    // x, y는 댕글링 참조!
    
    // ✅ 복사
    auto [x1, y1] = Point{10, 20};
    std::cout << "x1: " << x1 << ", y1: " << y1 << std::endl;
    
    // ✅ 변수 저장
    Point p = {10, 20};
    auto& [x2, y2] = p;
    x2 = 100;
    std::cout << "p.x: " << p.x << std::endl;  // 100
    
    return 0;
}

출력:

x1: 10, y1: 20
p.x: 100

문제 3: 타입 추론

#include <tuple>

int main() {
    // ❌ 타입 명시 불가
    // auto [int x, double y] = std::tuple{1, 2.0};  // 에러
    
    // ✅ auto만 가능
    auto [x, y] = std::tuple{1, 2.0};
    
    return 0;
}

5. 실무 패턴

패턴 1: 에러 처리

#include <iostream>
#include <string>
#include <fstream>

std::pair<bool, std::string> parseConfig(const std::string& path) {
    std::ifstream file(path);
    
    if (!file.is_open()) {
        return {false, "파일 없음"};
    }
    
    // 파싱 로직
    std::string content;
    if (!std::getline(file, content)) {
        return {false, "파싱 실패"};
    }
    
    return {true, "성공"};
}

int main() {
    auto [success, message] = parseConfig("config.json");
    
    if (!success) {
        std::cerr << "에러: " << message << std::endl;
        return 1;
    }
    
    std::cout << "결과: " << message << std::endl;
    
    return 0;
}

패턴 2: 다중 반환값

#include <tuple>
#include <iostream>

std::tuple<int, int, int> divmod(int dividend, int divisor) {
    int quotient = dividend / divisor;
    int remainder = dividend % divisor;
    int sign = (dividend < 0) ^ (divisor < 0) ? -1 : 1;
    
    return {quotient, remainder, sign};
}

int main() {
    auto [q, r, s] = divmod(17, 5);
    std::cout << "몫: " << q << ", 나머지: " << r << ", 부호: " << s << std::endl;
    // 몫: 3, 나머지: 2, 부호: 1
    
    auto [q2, r2, s2] = divmod(-17, 5);
    std::cout << "몫: " << q2 << ", 나머지: " << r2 << ", 부호: " << s2 << std::endl;
    // 몫: -3, 나머지: -2, 부호: -1
    
    return 0;
}

출력:

몫: 3, 나머지: 2, 부호: 1
몫: -3, 나머지: -2, 부호: -1

패턴 3: 범위 기반 for

#include <map>
#include <vector>
#include <iostream>
#include <string>

int main() {
    std::map<std::string, std::vector<int>> data = {
        {"A", {1, 2, 3}},
        {"B", {4, 5, 6}},
        {"C", {7, 8, 9}}
    };
    
    // 맵 순회
    std::cout << "=== 읽기 ===" << std::endl;
    for (const auto& [key, values] : data) {
        std::cout << key << ": ";
        for (int v : values) {
            std::cout << v << ' ';
        }
        std::cout << std::endl;
    }
    
    // 수정
    std::cout << "\n=== 수정 ===" << std::endl;
    for (auto& [key, values] : data) {
        values.push_back(0);  // 각 벡터에 0 추가
    }
    
    for (const auto& [key, values] : data) {
        std::cout << key << ": ";
        for (int v : values) {
            std::cout << v << ' ';
        }
        std::cout << std::endl;
    }
    
    return 0;
}

출력:

=== 읽기 ===
A: 1 2 3
B: 4 5 6
C: 7 8 9

=== 수정 ===
A: 1 2 3 0
B: 4 5 6 0
C: 7 8 9 0

6. 커스텀 타입 지원

#include <iostream>

class MyClass {
public:
    int x, y;
    
    MyClass(int x, int y) : x(x), y(y) {}
    
    // 튜플 프로토콜 구현
    template<size_t I>
    auto& get() {
        if constexpr (I == 0) return x;
        else if constexpr (I == 1) return y;
    }
    
    template<size_t I>
    const auto& get() const {
        if constexpr (I == 0) return x;
        else if constexpr (I == 1) return y;
    }
};

// 특수화
namespace std {
    template<>
    struct tuple_size<MyClass> : integral_constant<size_t, 2> {};
    
    template<size_t I>
    struct tuple_element<I, MyClass> {
        using type = int;
    };
}

int main() {
    MyClass obj{10, 20};
    auto [a, b] = obj;
    
    std::cout << a << ", " << b << std::endl;  // 10, 20
    
    return 0;
}

7. 실전 예제: 데이터 처리

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

struct Student {
    std::string name;
    int score;
    std::string grade;
};

std::map<std::string, std::vector<Student>> groupByGrade(const std::vector<Student>& students) {
    std::map<std::string, std::vector<Student>> groups;
    
    for (const auto& [name, score, grade] : students) {
        groups[grade].push_back({name, score, grade});
    }
    
    return groups;
}

void printStatistics(const std::map<std::string, std::vector<Student>>& groups) {
    for (const auto& [grade, students] : groups) {
        int total = 0;
        for (const auto& [name, score, g] : students) {
            total += score;
        }
        
        double average = static_cast<double>(total) / students.size();
        
        std::cout << "등급 " << grade << ": " 
                  << students.size() << "명, 평균 " 
                  << average << "점" << std::endl;
    }
}

int main() {
    std::vector<Student> students = {
        {"Alice", 95, "A"},
        {"Bob", 85, "B"},
        {"Charlie", 92, "A"},
        {"David", 78, "C"},
        {"Eve", 88, "B"}
    };
    
    auto groups = groupByGrade(students);
    
    std::cout << "=== 학생 목록 ===" << std::endl;
    for (const auto& [grade, studentList] : groups) {
        std::cout << "등급 " << grade << ":" << std::endl;
        for (const auto& [name, score, g] : studentList) {
            std::cout << "  " << name << ": " << score << "점" << std::endl;
        }
    }
    
    std::cout << "\n=== 통계 ===" << std::endl;
    printStatistics(groups);
    
    return 0;
}

출력:

=== 학생 목록 ===
등급 A:
  Alice: 95점
  Charlie: 92점
등급 B:
  Bob: 85점
  Eve: 88점
등급 C:
  David: 78점

=== 통계 ===
등급 A: 2명, 평균 93.5점
등급 B: 2명, 평균 86.5점
등급 C: 1명, 평균 78점

정리

핵심 요약

  1. 구조적 바인딩: 튜플/배열/구조체 분해
  2. auto: 복사, auto&: 참조, const auto&: const 참조
  3. 맵 순회: for (const auto& [key, value] : map)
  4. 함수 반환: auto [a, b] = func()
  5. 성능: 컴파일 타임, 오버헤드 없음

지원 타입

타입예시설명
배열int arr[3]고정 크기 배열
튜플std::tuple<int, double>std::tuple, std::pair
구조체struct Point { int x, y; }집합체 (aggregate)
커스텀get<I>() 구현튜플 프로토콜

실전 팁

사용 원칙:

  • 맵 순회: const auto& [key, value]
  • 함수 반환: auto [a, b] = func()
  • 읽기만: const auto&
  • 수정: auto&

성능:

  • 컴파일 타임 처리
  • 런타임 오버헤드 없음
  • 복사 방지 (참조 사용)
  • 가독성 향상

주의사항:

  • 요소 개수 일치
  • 참조 수명 관리
  • 타입 명시 불가
  • 중첩 분해 불가 (C++17)

다음 단계

  • C++ Tuple
  • C++ Range-based For
  • C++ Auto Keyword

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

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

  • C++ Structured Binding 고급 | “구조화 바인딩” 가이드
  • C++ 범위 기반 for문과 구조화된 바인딩 | 모던 C++ 반복문
  • C++ tuple apply | “튜플 적용” 가이드

관련 글

  • C++ 범위 기반 for문과 구조화된 바인딩 | 모던 C++ 반복문
  • C++ Structured Binding 고급 |
  • C++ any |
  • C++ auto 키워드 |
  • C++ auto 타입 추론 | 복잡한 타입을 컴파일러에 맡기기