NeuroWhAI의 잡블로그

[C++] Structured binding 설명 본문

개발 및 공부/언어

[C++] Structured binding 설명

NeuroWhAI 2018. 9. 20. 21:25


https://en.cppreference.com/w/cpp/language/structured_binding



Structured binding은 C++17에서 추가된 문법입니다.

한글로는 뭐라고 해야할지 모르겠네요. 구조적 바인딩?

구조체나 배열 등이 가지는 멤버들을 풀어서 바인딩(변수에 할당)할 수 있게 해주는 편의 문법입니다.


가장 간단한 예를 들자면

int arr[3] = { 1, 2, 3 };
auto [a, b, c] = arr;
cout << a << b << c << endl;
// 출력 : 123

이런 모습입니다.

직관적이라서 쉽게 사용할 수 있습니다.


사실 위 예시는 그냥 arr[0], arr[1], arr[2]로도 충분하다고 볼 수 있지만 C++17의 다른 문법들과 함께 사용하면 코드를 매우 편리하게 작성할 수 있습니다.

아래 예제를 봅시다.

// 문자를 받아 숫자 문자이면 해당하는 숫자의 정수로 변환하는 함수.
// 반환되는 튜플은 tuple<정수, 성공 여부, 에러 내용>로 되어있다.
std::tuple<int, bool, std::string> charToInt(char c);

int main()
{
    // Before C++17
    auto result = charToInt('7');
    if (std::get<1>(result))
    {
        cout << std::get<0>(result) << endl;
    }
    else
    {
        cout << "Error: " << std::get<2>(result) << endl;
    }
    
    // After C++17
    if (auto [num, success, err] = charToInt('?');
        success)
    {
        cout << num << endl;
    }
    else
    {
        cout << "Error: " << err << endl;
    }
    
    return 0;
}

tuple이나 pair를 사용하면 각 요소로의 접근이 상당히 비직관적(get, tie, first, second 등)이었는데

Structured binding 문법을 사용함으로서 이해하기 쉬운 코드가 되었습니다.

(참고로 if (...; ...) { ... }도 C++17에서 추가된 문법으로 for처럼 초기화, 조건 등으로 여러 실행문이 나눠져 있는거라고 보시면 됩니다)


다른 예제도 봅시다.

std::map<int, int> m;
m[1] = 2;
m[2] = 4;
m[3] = 6;

// Before C++17
for (const auto& kv : m)
{
    cout << "m[" << kv.first << "] = " << kv.second << endl;
}

// After C++17
for (const auto& [key, value] : m)
{
    cout << "m[" << key << "] = " << value << endl;
}

보시면 map의 요소를 순회하는데에도 편리하게 사용할 수 있다는걸 알 수 있죠.

kv가 pair이므로 Structured binding 문법을 이용해서 분해(?)할 수 있었습니다.


예제는 이쯤하면 충분하니 이제 문법을 좀 더 깊게 공부해봅시다.


cppreference.com에서는 Structured binding이 가능한 경우를 3개로 분류하여 설명하고 있습니다.


Case 1: 배열

이 글 처음에 보여드린 예제에 해당하는 경우입니다.

직관적으로 배열의 요소들이 각각의 식별자들(auto [식별자들] = ...)에 할당됩니다.

이때 그냥 auto라면 값이 복사(int a = arr[0])가 되며 auto&로 해줘야 참조(int& a = arr[0])가 됩니다.

그리고 auto에 const나 volatile같은 지정자를 붙히면 그대로 각 분해된 변수에도 적용이 됩니다.

ex) const auto& [a, b, c] = arr;

반대로 배열에 const나 volatile같은 지정자가 붙어있다면 그냥 auto&로 해도 해당 지정자가 붙습니다.


Case 2: tuple-like(튜플스러운) 타입

튜플스럽다고 하면 분해할 타입을 E라고 했을 때 std::tuple_size<E>::value가 올바르게 정의되어 있음을 뜻합니다.

반대로 std::tuple_size<E>::value가 정의되어 있다면 tuple-like로 취급된다는 말입니다.

각 요소는 먼저 해당 타입에 get 메소드(e.get<i>())가 있다면 그걸 쓰고 없다면 ADL로 찾은 get 함수(get<i>(e))를 사용합니다.

그리고 이 경우엔 바인딩된 각 식별자들의 타입이 비직관적인 규칙으로 설정되니 주의해야 합니다.

Structured binding은 내부적으로 임시 변수(e라고 부르겠습니다)를 생성해서 분해할 값을 받아두는데 이때 e의 타입(E)이 '['문자 전까지의 선언 타입으로 지정됩니다.

예를 들어 const auto& [ a, b, c ] = ...;일때 e의 타입은 const auto&가 되는 것이죠.

a, b, c의 타입이 const auto&가 되는게 아닙니다!

때문에 배열의 경우엔 '&'를 붙히거나 const를 붙힘으로서 식별자의 타입에 영향을 끼칠 수 있었는데 이번엔 그렇지 않은 경우가 있게 됩니다.

그럼 a, b, c의 타입은 무엇으로 결정되느냐? 기본적으로 E 타입의 i번째 요소의 타입을 뜻하는 std::tuple_element<i, E>::type로 설정됩니다.

그러니까

std::tuple<float&, char&&, int> tpl(x,std::move(y), z);
const auto& [a, b, c] = tpl;

이러할때 a는 float&, b는 char&&, c는 const int가 됩니다.

더 구체적으로 풀어서 적자면 아래와 비슷하게 되니 참고하시기 바랍니다.

//const auto& [a, b, c] = tpl;
const auto& e = tpl;
using E = std::remove_reference_t<decltype((e))>;
std::tuple_element_t<0, E>& a = std::get<0>(e);[각주:1]
std::tuple_element_t<1, E>& b = std::get<1>(e);
std::tuple_element_t<2, E>& c = std::get<2>(e);

흠.. 그러나 왜 이런 비직관적인 규칙을 사용하는지는 잘 모르겠습니다 ㅠㅠ


Case3: 데이터 멤버들

struct S {
    int x1 : 2;
    volatile double y1;
};
S f();
 
const auto [x, y] = f();

간단합니다.

구조체나 클래스 객체의 필드가 모두 접근 가능해야하며 그 중 비정적(non-static) 멤버만 분해됩니다.

이런 제약이 있기 때문에 tuple-like로 만들어서 사용해야할 상황이 많이 생길 듯 합니다.

참고로 위 예제에서 x는 2 bit 크기의 const int이며 y는 const volatile double입니다.


이상입니다. 감사합니다!



  1. e가 const&임에도 get으로 const&가 아닌 &를 얻을 수 있는 이유는 const& 파라미터를 받는 오버로딩 버전인 get의 반환형이 std::tuple_element::type const&이기 때문입니다. 0번을 예로 들면 type은 int&가 되고 따라서 반환형은 int& const&가 되는데 이게 reference collapsing 규칙에 따라 int&로 바뀌기 때문입니다. [본문으로]


Comments