[C++23 새기능 소개] std::views::zip & std::views::zip_transform

C++23에서는 범위(Range) 라이브러리에 더욱 강력한 조합 기능을 제공하기 위해 std::views::zipstd::views::zip_transform 뷰 어댑터가 도입되었습니다. 이들 뷰는 복수의 범위를 하나로 묶어서 각 원소를 튜플 형태로 병합하거나, 사용자 지정 함수로 변환할 수 있습니다. 이를 통해 여러 컨테이너나 시퀀스를 동기적으로 처리하고, 직관적으로 결합하는 로직을 간결하게 표현할 수 있습니다. 이번 글에서는 std::views::zip과 std::views::zip_transform의 개념과 사용법, 그리고 이전 방식과 비교하여 어떤 개선점을 제공하는지 알아보겠습니다.

std::views::zip와 std::views::zip_transform란 무엇인가요?

  • std::views::zip: 여러 범위를 동시에 순회하며, 각 인덱스에 해당하는 원소들을 튜플로 묶어 하나의 범위를 생성합니다. 예를 들어, 두 개의 벡터가 있을 때, (v1[i], v2[i]) 형태의 튜플을 원소로 하는 범위를 제공할 수 있습니다.
  • std::views::zip_transform: std::views::zip와 유사하지만, 각 튜플 원소를 그대로 반환하는 대신 사용자 지정 함수를 적용하여 변환한 결과를 반환합니다. 즉, (v1[i], v2[i])에서 바로 (v1[i] + v2[i])와 같은 결과를 얻는 등의 작업을 쉽게 처리할 수 있습니다.

이러한 뷰들은 정렬된 데이터를 병합하거나, 여러 컨테이너를 동기적으로 처리해야 하는 로직에서 큰 가치를 발휘합니다.

이전 버전에서는 어떻게 했나요?

C++20까지는 여러 컨테이너를 동시에 순회하려면 인덱스를 사용하거나, iterator를 수동으로 맞춰가며 처리해야 했습니다.

예제: 기존 방식 (C++20까지)

#include <vector>
#include <iostream>

int main() {
    std::vector<int> v1 = {1, 2, 3};
    std::vector<double> v2 = {4.0, 5.0, 6.0};

    // 두 벡터를 동시에 순회하여 합산 결과를 출력
    for (std::size_t i = 0; i < v1.size() && i < v2.size(); ++i) {
        std::cout << v1[i] + v2[i] << ' ';
    }
    return 0;
}
  • 문제점: 인덱스 기반 접근으로 가독성이 떨어지고, v1과 v2의 크기 비교 등을 매번 수동 처리해야 합니다. 함수형 스타일로 데이터 결합을 표현하기 어렵습니다.

C++23의 std::views::zip 사용 예제

#include <ranges>
#include <vector>
#include <iostream>

int main() {
    std::vector<int> v1 = {1, 2, 3};
    std::vector<double> v2 = {4.0, 5.0, 6.0};

    // zip을 사용하면 (1,4.0), (2,5.0), (3,6.0)을 원소로 하는 범위를 얻음
    for (auto&& [x, y] : std::views::zip(v1, v2)) {
        std::cout << x + y << ' '; // 5, 7, 9 출력
    }

    return 0;
}
  • std::views::zip을 통해 v1과 v2를 동기적으로 처리하기가 훨씬 간단해졌습니다.
  • 구조적 바인딩을 사용하여 튜플 원소에 직접 접근할 수 있고, 크기 안맞음 등에 대해서는 한쪽 범위가 끝날 때 순회가 종료됩니다.

std::views::zip_transform 사용 예제

#include <ranges>
#include <vector>
#include <iostream>

int main() {
    std::vector<int> v1 = {10, 20, 30};
    std::vector<int> v2 = {1, 2, 3};

    // zip_transform을 사용하여 각 튜플 원소를 즉시 변환
    // 여기서는 (v1[i], v2[i]) -> v1[i] * v2[i]로 변환
    auto result_view = std::views::zip_transform([](int a, int b) { return a * b; }, v1, v2);

    for (auto val : result_view) {
        std::cout << val << ' '; // 10*1=10, 20*2=40, 30*3=90 출력
    }

    return 0;
}
  • std::views::zip_transform을 사용하면 zip 후 별도의 transform 과정을 거치지 않고 한 번에 원하는 결과를 생성할 수 있어 코드가 더욱 깔끔해집니다.

어떻게 좋아졌나요?

  • 코드 가독성 향상: 인덱스 기반 접근 필요 없이, 명확하게 여러 범위를 병합하고 변환하는 로직을 표현.
  • 함수형 스타일: zip과 transform을 파이프라인 형태로 합칠 수 있어 함수형 프로그래밍 스타일에 가까운 코드를 구현.
  • 오류 감소: 인덱스 관리, 크기 비교 등의 보일러플레이트 코드가 줄어들어 실수 발생 가능성 감소.
  • 유연한 조합: 다른 범위 어댑터들과 쉽게 조합할 수 있어 복잡한 데이터 처리 파이프라인을 간결하게 표현.

상세한 예제와 비교

기존 방식 vs zip/zip_transform

기존 방식(C++20 이전)

for (std::size_t i = 0; i < std::min(v1.size(), v2.size()); ++i) {
    std::cout << v1[i] + v2[i] << ' ';
}

C++23 zip_transform 방식

for (auto val : std::views::zip_transform(std::plus{}, v1, v2)) {
    std::cout << val << ' ';
}
  • 한 줄로 더 명확하게 표현 가능

주의 사항

  • 범위 크기 차이: zip 및 zip_transform은 가장 짧은 범위가 끝나면 순회를 종료합니다. 길이가 다른 범위를 처리 시 의도한 동작인지 확인 필요.
  • 컴파일러 지원: C++23 기능이므로, 해당 기능을 지원하는 컴파일러와 표준 라이브러리 필요.
  • 인자 타입 일관성: zip_transform 사용 시 람다나 함수 객체의 인자 타입과 리턴 타입이 올바른지 체크.

요약

C++23의 std::views::zip 및 std::views::zip_transform는 여러 범위를 동시에 처리하고, 각 원소를 튜플 형태로 묶거나 즉시 변환할 수 있는 강력한 뷰 어댑터입니다. 이를 통해 인덱스 기반 접근 없이 직관적으로 여러 컨테이너를 합쳐 처리하거나, 변환 로직을 우아하게 표현할 수 있습니다. 범위 라이브러리의 풍부한 조합 기능과 결합하면, 복잡한 데이터 파이프라인을 간결한 코드로 구현할 수 있어 코드 품질과 생산성을 향상시킵니다.

 

참고 자료:

반응형