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 뷰에 구분자 삽입 기능을 더해, 중첩 범위를 평탄화하는 동시에 부분 범위 사이에 원하는 구분자를 간편히 삽입할 수 있도록 합니다. 이를 통해 문자열, 숫자열 등 다양한 데이터 시퀀스를 직관적이고 간결하게 처리할 수 있으며, 다른 범위 어댑터와의 조합으로 복잡한 데이터 파이프라인을 더욱 깔끔하게 구현할 수 있습니다.
참고 자료:
'개발 이야기 > C++' 카테고리의 다른 글
[C++23 새기능 소개] [[nodiscard("이유")]] 속성 강화 (0) | 2024.12.09 |
---|---|
[C++23 새기능 소개] std::views::chunk & std::views::chunk_by (0) | 2024.12.09 |
[C++23 새기능 소개] std::views::zip & std::views::zip_transform (0) | 2024.12.09 |
[C++23 새기능 소개] std::ranges::lazy_split_view (0) | 2024.12.09 |
[C++23 새기능 소개] 개선된 constexpr 컨테이너 지원 (1) | 2024.12.09 |