[C++23 새기능 소개] std::views::join_with

C++23에서는 범위(Range) 라이브러리를 더욱 편리하게 다룰 수 있도록 다양한 뷰(View) 어댑터가 추가되었는데, 그중 하나가 바로 std::views::join_with 입니다. 기존에 C++20에서 도입된 std::views::join 뷰는 중첩된 범위를 평탄화(flatten)하는 기능을 제공했지만, join_with는 여기서 한 걸음 더 나아가 두 범위 사이에 지정한 구분자(delimiter) 범위를 삽입하는 기능을 제공합니다.

 

이번 글에서는 std::views::join_with의 개념과 사용법, 그리고 이전 버전과 비교하여 이 기능을 통해 어떤 점이 개선되었는지 살펴보겠습니다.

std::views::join_with란 무엇인가요?

std::views::join_with는 중첩된 범위를 하나의 연속적인 범위로 평탄화(flatten)하면서, 각 부분 범위 사이에 지정한 구분자 범위를 삽입하는 뷰 어댑터입니다.

  • 예를 들어, std::views::join은 [[1,2,3], [4,5], [6]]와 같은 중첩 범위를 [1,2,3,4,5,6]로 평탄화할 수 있습니다.
  • std::views::join_with는 여기서 한 발 더 나아가 각 부분 범위 사이에 구분자 범위를 삽입할 수 있으므로, [[1,2,3], [4,5], [6]]를 만약 구분자로 [0,0]을 지정한다면 [1,2,3,0,0,4,5,0,0,6]와 같은 결과를 얻을 수 있습니다.

이 기능을 통해 중첩 범위를 다룰 때 훨씬 더 유연한 결과물을 생성할 수 있으며, 문자열이나 토큰 리스트를 원하는 형식으로 조립하는데 특히 유용할 수 있습니다.

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

C++20의 std::views::join을 사용하면 중첩된 범위를 평탄화할 수는 있었지만, 각 부분 범위 사이에 별도의 구분자를 삽입하기 위해서는 수동으로 insert 연산을 수행하거나, join 결과를 추가 변환하는 로직을 별도로 구현해야 했습니다.

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

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

// 중첩된 벡터: [[1,2,3], [4,5], [6]]
int main() {
    std::vector<std::vector<int>> nested = {{1,2,3}, {4,5}, {6}};
    // join으로 평탄화: [1,2,3,4,5,6]
    auto flat = nested | std::views::join;

    // 만약 [1,2,3,0,0,4,5,0,0,6] 형태를 만들고 싶다면?
    // join 결과를 다시 처리하거나 수동으로 0,0 삽입하는 별도 로직 필요
    // 코드가 복잡해지고 가독성 저하
    for (auto x : flat) {
        std::cout << x << ' ';
    }
    return 0;
}
  • 여기서 0,0과 같은 구분자를 삽입하기 위해서는 join 결과를 별도 벡터에 복사하면서 중간에 0,0을 삽입하는 등 추가 코드가 필요했습니다.

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

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

int main() {
    std::vector<std::vector<int>> nested = {{1,2,3}, {4,5}, {6}};
    std::vector<int> delimiter = {0,0};

    // join_with를 사용하여 각 부분 범위 사이에 {0,0} 삽입
    auto joined = nested | std::views::join_with(delimiter);

    for (auto x : joined) {
        std::cout << x << ' '; 
    }
    // 출력: 1 2 3 0 0 4 5 0 0 6
    return 0;
}
  • 이제 한 줄의 파이프라인으로 평탄화와 구분자 삽입을 동시에 처리할 수 있습니다.
  • 중첩된 범위의 각 구간 사이에 자동으로 구분자 범위가 들어가므로, 별도의 후처리 로직이 필요 없습니다.

어떻게 좋아졌나요?

  • 코드 가독성 및 간결성 향상: 한 번의 뷰 어댑터 적용으로 평탄화와 구분자 삽입을 처리할 수 있어 로직이 단순해집니다.
  • 유연한 파이프라인 구성: 다른 범위 어댑터(filter, transform, take, drop 등)와 쉽게 조합 가능하므로 복잡한 데이터 파이프라인을 더 단순하게 표현할 수 있습니다.
  • 직관적 표현: 구분자를 삽입하는 로직을 수동으로 구현할 필요가 없어, 의도를 명확하게 드러냅니다.

상세한 예제와 비교

문자열 처리

문자열 토큰 리스트를 특정 구분자로 합치는 작업에 유용할 수 있습니다.

#include <ranges>
#include <string_view>
#include <iostream>

int main() {
    std::vector<std::string_view> words = {"Hello", "World", "C++23"};
    std::string_view delimiter = " ";

    auto joined = words | std::views::join_with(delimiter);
    for (char c : joined) {
        std::cout << c;
    }
    // 출력: "Hello World C++23"
    return 0;
}
  • 이전에는 join 후 구분자를 삽입하기 위해 별도의 알고리즘을 작성해야 했을 수도 있었지만, 이제는 한 번의 파이프라인으로 해결됩니다.

다른 뷰와의 조합

#include <ranges>
#include <string_view>
#include <iostream>

int main() {
    std::vector<std::vector<int>> nested = {{1,2},{3},{4,5,6}};
    std::vector<int> delimiter = {-1};

    // odd number만 남기고, 그 사이에 -1 삽입
    auto filtered_and_joined = nested
        | std::views::transform([](auto vec) {
            return vec | std::views::filter([](int x){ return x % 2 == 1; });
        })
        | std::views::join_with(delimiter);

    for (int x : filtered_and_joined) {
        std::cout << x << ' ';
    }
    // 출력: 1 -1 3 -1 5
    // (4,6은 짝수여서 제거됨)
    return 0;
}

주의 사항

  • 구분자 범위 타입: 구분자로 사용할 범위는 std::views::join_with가 요구하는 범위 개념을 만족해야 합니다.
  • 범위 길이 관리: join_with 결과 범위는 원본 범위의 부분 범위 개수와 구분자 범위를 합한 길이에 따라 달라집니다. 길이를 추론하기 쉽지 않을 수 있으니 주의.
  • 컴파일러 및 라이브러리 지원: C++23 기능이므로, 해당 기능을 지원하는 컴파일러와 표준 라이브러리가 필요합니다.

요약

C++23의 std::views::join_with는 기존 join 뷰에 구분자 삽입 기능을 더해, 중첩 범위를 평탄화하는 동시에 부분 범위 사이에 원하는 구분자를 간편히 삽입할 수 있도록 합니다. 이를 통해 문자열, 숫자열 등 다양한 데이터 시퀀스를 직관적이고 간결하게 처리할 수 있으며, 다른 범위 어댑터와의 조합으로 복잡한 데이터 파이프라인을 더욱 깔끔하게 구현할 수 있습니다.

 

참고 자료:

반응형