[C++] 구조적 바인딩 : auto [a, b] = …

[C++] 구조적 바인딩 : auto [a, b] = …

안녕하세요. 이번 글에서는 C++17에서 추가된 구조적 바인딩(structured bindings)에 대해 알아보겠습니다.

std::pair나 std::tuple, 구조체의 값을 꺼낼 때 first, second, std::get<N>을 반복해서 쓰면 코드가 길어지고 의미도 잘 드러나지 않습니다. 구조적 바인딩은 여러 값을 한 번에 이름을 붙여 풀어내는 문법입니다.

기본 사용법 : auto [a, b] = …

auto [이름1, 이름2, …] = 객체; 형태로 선언합니다. 컴파일 옵션은 C++17 이상(-std=c++17)이 필요합니다.

#include <iostream>
#include <string>
#include <tuple>
using namespace std;

tuple<int, string, double> GetInfo() { return {1, "Alice", 3.5}; }

int main()
{
    pair<string, int> p{"apple", 3};
    auto [name, count] = p;                 // first, second 대신 이름 사용
    cout << name << " " << count << "\n";   // apple 3

    auto [id, nm, score] = GetInfo();       // tuple도 가능
    cout << id << " " << nm << " " << score << "\n";
}

구조체와 배열

pair, tuple뿐 아니라 public 멤버만 가진 구조체와 배열도 풀 수 있습니다. 구조체는 멤버가 선언된 순서대로 이름이 붙습니다.

struct Point { int x; int y; };

Point pt{3, 4};
auto [x, y] = pt;           // x = 3, y = 4

int arr[3] = {1, 2, 3};
auto [a, b, c] = arr;       // a = 1, b = 2, c = 3

이름의 개수는 멤버(원소) 개수와 정확히 같아야 합니다. 일부만 받고 싶어도 개수를 맞춰야 하며, 쓰지 않는 값에도 이름을 붙여야 합니다.

map 순회에서 활용

구조적 바인딩이 가장 많이 쓰이는 곳은 map의 범위 기반 for문입니다. it->first, it->second 대신 의미 있는 이름을 쓸 수 있습니다.

map<string, int> scores{{"kim", 90}, {"lee", 85}};

// before
for (const auto& kv : scores)
    cout << kv.first << "=" << kv.second << "\n";

// after
for (const auto& [k, v] : scores)
    cout << k << "=" << v << "\n";

복사와 참조 : auto, auto&, const auto&

auto 앞에 붙이는 수식어가 동작을 결정합니다. 구조적 바인딩은 객체를 먼저 숨은 변수로 만들고, 각 이름이 그 멤버를 가리키는 방식입니다.

선언동작
auto [a, b] = obj;obj를 복사한 뒤 그 복사본의 멤버에 바인딩합니다. a, b를 바꿔도 obj에는 영향이 없습니다.
auto& [a, b] = obj;obj 자체에 바인딩합니다. a, b를 바꾸면 obj가 바뀝니다.
const auto& [a, b] = obj;복사 없이 읽기 전용으로 바인딩합니다. 큰 객체에 적합합니다.
Point q{1, 2};
auto& [rx, ry] = q;
rx = 10;
cout << q.x << "\n";   // 10

if 초기화문과 함께 쓰기

C++17의 if 초기화문과 조합하면 insert의 반환값(iterator, bool)을 간결하게 다룰 수 있습니다.

if (auto [it, ok] = scores.insert({"park", 70}); ok)
    cout << "inserted " << it->first << "\n";

auto [it2, ok2] = scores.insert({"kim", 1});
cout << ok2 << "\n";   // 0 (이미 존재하므로 삽입되지 않음)

주의할 점

  • 이름의 개수가 멤버 개수와 다르면 컴파일 오류가 발생합니다.
  • auto 하나로만 선언할 수 있어서 [int a, double b]처럼 이름마다 타입을 따로 지정할 수는 없습니다.
  • private 멤버가 있는 클래스는 풀 수 없습니다. 멤버가 모두 public이고 상속 구조가 복잡하지 않아야 합니다.
  • C++17에서는 구조적 바인딩으로 만든 이름을 람다에서 캡처할 수 없고, C++20부터 가능합니다.
  • 반환값을 일부러 무시하는 문법(_)은 C++17에 없으므로 쓰지 않는 이름도 선언해야 합니다.

정리

구조적 바인딩은 pair, tuple, 구조체, 배열의 값을 의미 있는 이름으로 한 번에 풀어 줍니다. 복사가 필요 없을 때는 const auto&, 값을 바꿔야 할 때는 auto&를 사용하면 되고, map 순회와 insert 결과 확인에서 특히 코드가 짧아집니다.

이상으로 구조적 바인딩에 대해 간단히 알아보았습니다.

참고