콘텐츠로 이동

C++17 Structured Bindings 완벽 가이드

C++17의 Structured Bindings(구조적 바인딩)는 배열, 구조체, 튜플, pair 등 복합 타입의 요소들을 개별 변수에 한 번에 바인딩할 수 있게 해주는 문법입니다. Python의 언패킹과 유사하지만 C++의 타입 시스템 안에서 동작합니다.


auto [변수1, 변수2, ...] = 표현식;

auto 뒤에 대괄호로 변수명 목록을 지정하면, 우변 표현식의 각 요소가 해당 변수에 바인딩됩니다.


#include <iostream>
int arr[3] = {10, 20, 30};
auto [a, b, c] = arr; // a=10, b=20, c=30
std::cout << a << ", " << b << ", " << c << "\n"; // 10, 20, 30
#include <map>
#include <string>
std::map<std::string, int> scores = {{"Alice", 95}, {"Bob", 87}};
for (const auto& [name, score] : scores) {
std::cout << name << ": " << score << "\n";
}
// Alice: 95
// Bob: 87
#include <tuple>
auto getData() {
return std::make_tuple(42, 3.14, std::string("hello"));
}
auto [id, ratio, label] = getData();
std::cout << id << ", " << ratio << ", " << label << "\n"; // 42, 3.14, hello

멤버 변수가 public인 경우 자동으로 분해됩니다.

struct Point {
double x;
double y;
double z;
};
Point origin{1.0, 2.0, 3.0};
auto [px, py, pz] = origin;
std::cout << px << ", " << py << ", " << pz << "\n"; // 1, 2, 3

auto&로 선언하면 원본 데이터에 대한 참조로 바인딩됩니다.

std::pair<int, std::string> item{1, "apple"};
auto& [id, name] = item;
name = "banana"; // item.second도 변경됨
std::cout << item.second << "\n"; // banana

const auto&는 읽기 전용 참조입니다.

for (const auto& [key, val] : myMap) {
// key, val 수정 불가
std::cout << key << ": " << val << "\n";
}

컴파일러는 structured binding을 다음과 같이 변환합니다.

// 원본 코드
auto [x, y] = std::make_pair(1, 2);
// 컴파일러가 생성하는 코드 (개념적 표현)
auto __e = std::make_pair(1, 2);
using __E = decltype(__e);
auto& x = std::get<0>(__e);
auto& y = std::get<1>(__e);

실제로는 각 바인딩 변수가 내부 임시 객체의 멤버에 대한 참조로 처리됩니다. 따라서 임시 객체의 생명주기가 바인딩 변수의 생명주기와 일치합니다.


5. 사용자 정의 타입에 get<> 지원 추가

섹션 제목: “5. 사용자 정의 타입에 get<> 지원 추가”

std::get<>std::tuple_size, std::tuple_element를 특수화하면 임의의 타입도 structured binding을 지원할 수 있습니다.

#include <tuple>
class Color {
public:
uint8_t r, g, b;
Color(uint8_t r, uint8_t g, uint8_t b) : r(r), g(g), b(b) {}
};
// std::tuple_size 특수화
template<>
struct std::tuple_size<Color> : std::integral_constant<std::size_t, 3> {};
// std::tuple_element 특수화
template<std::size_t I>
struct std::tuple_element<I, Color> {
using type = uint8_t;
};
// get<> 함수 구현
template<std::size_t I>
uint8_t get(const Color& c) {
if constexpr (I == 0) return c.r;
else if constexpr (I == 1) return c.g;
else return c.b;
}
// 이제 structured binding 사용 가능
Color red{255, 0, 0};
auto [r, g, b] = red;
std::cout << (int)r << ", " << (int)g << ", " << (int)b << "\n"; // 255, 0, 0

struct ParseResult {
bool success;
int value;
std::string error;
};
ParseResult parseInteger(const std::string& s) {
try {
return {true, std::stoi(s), ""};
} catch (...) {
return {false, 0, "Invalid integer: " + s};
}
}
auto [ok, val, err] = parseInteger("42");
if (ok) {
std::cout << "Parsed: " << val << "\n";
} else {
std::cout << "Error: " << err << "\n";
}
std::map<std::string, int> registry;
auto [it, inserted] = registry.emplace("item", 42);
if (inserted) {
std::cout << "새 항목 삽입: " << it->second << "\n";
} else {
std::cout << "이미 존재: " << it->second << "\n";
}
#include <ranges>
std::vector<std::string> items = {"alpha", "beta", "gamma"};
for (auto [i, val] : std::views::enumerate(items)) { // C++23
std::cout << i << ": " << val << "\n";
}

  • 바인딩 변수 수는 분해되는 요소 수와 정확히 일치해야 합니다.
  • auto&&는 universal reference로 동작하여 이동 의미론을 활용할 수 있습니다.
  • 중첩 structured binding은 지원되지 않습니다(auto [[a,b], c] 불가).
  • 비트필드에는 structured binding이 적용되지 않습니다.

std::map<std::string, std::vector<int>> data = {
{"scores", {90, 85, 92}},
{"ages", {25, 30, 28}}
};
// auto — 값 복사 (데이터 복사 비용 발생)
for (auto [key, values] : data)
{
// key와 values는 복사본 — 원본 변경 안 됨
values.push_back(0); // data에 영향 없음
}
// const auto& — 읽기 전용 참조 (가장 일반적)
for (const auto& [key, values] : data)
{
std::cout << key << ": " << values.size() << " items\n";
// values.push_back(0); // 오류: const 참조
}
// auto& — 읽기/쓰기 참조 (원본 수정)
for (auto& [key, values] : data)
{
values.push_back(0); // data 내용이 실제로 변경됨
}
// auto&& — 유니버설 레퍼런스 (이동 의미론 활용)
for (auto&& [key, values] : data)
{
// 임시 객체라면 이동, lvalue라면 참조로 바인딩
}

// C++20: structured binding을 람다에서 캡처
std::pair<int, std::string> p{42, "hello"};
auto [num, str] = p;
// C++20 이전: structured binding 변수를 람다에서 직접 캡처 불가
// C++20 이후: 가능
auto lambda = [num, str]() { // C++20: 캡처 가능
std::cout << num << " " << str << "\n";
};
lambda();
// C++23: views::enumerate와 결합
#include <ranges>
std::vector<std::string> names = {"Alice", "Bob", "Carol"};
for (auto [index, name] : std::views::enumerate(names))
{
std::cout << index << ": " << name << "\n";
}
// 0: Alice
// 1: Bob
// 2: Carol

10. 실전 활용 — 알고리즘 반환값 처리

섹션 제목: “10. 실전 활용 — 알고리즘 반환값 처리”
#include <algorithm>
#include <vector>
#include <string>
// std::minmax_element 반환값 분해
std::vector<int> scores = {85, 92, 78, 95, 88};
auto [min_it, max_it] = std::minmax_element(scores.begin(), scores.end());
std::cout << "최소: " << *min_it << ", 최대: " << *max_it << "\n";
// std::equal_range 반환값 분해
std::vector<int> sorted = {1, 2, 2, 3, 3, 3, 4};
auto [lower, upper] = std::equal_range(sorted.begin(), sorted.end(), 3);
std::cout << "3의 개수: " << std::distance(lower, upper) << "\n"; // 3
// std::mismatch 반환값 분해
std::string s1 = "hello world";
std::string s2 = "hello C++";
auto [it1, it2] = std::mismatch(s1.begin(), s1.end(), s2.begin());
std::cout << "첫 차이: '" << *it1 << "' vs '" << *it2 << "'\n";

Structured Bindings는 코드의 가독성을 크게 향상시키는 C++17의 핵심 기능입니다. 특히 범위 기반 for 루프에서 map을 순회하거나, 함수에서 여러 값을 반환받을 때 강력한 표현력을 발휘합니다.

한정자의미사용 시점
auto값 복사작은 타입, 수정 불필요
const auto&읽기 전용 참조읽기 전용 순회 (가장 흔함)
auto&읽기/쓰기 참조원본 수정 필요
auto&&유니버설 참조이동 의미론, 제네릭 코드

핵심 규칙:

  • 바인딩 변수 수는 분해 요소 수와 정확히 일치해야 함
  • 중첩 structured binding(auto [[a,b], c]) 불가
  • 비트필드에는 적용 불가
  • C++20부터 람다에서 캡처 가능