C++23에서는 범위(Range) 라이브러리를 풍성하게 하는 새로운 뷰(View) 어댑터들이 다양하게 추가되었습니다. 그중 하나가 바로 std::views::split_when 인데, 이 뷰 어댑터는 std::views::split와 유사한 역할을 하지만, 단순한 구분 문자나 구분 값이 아닌 **사용자 정의 조건자(predicate)**에 따라 범위를 동적으로 분할할 수 있습니다. 이를 통해 보다 유연하게 범위를 나누고, 특정 패턴이나 조건을 만족하는 지점마다 분리하는 로직을 간결하고 직관적으로 표현할 수 있습니다.
이번 글에서는 std::views::split_when의 개념과 사용법, 그리고 이전 방식과 비교하여 어떤 점이 개선되었는지 알아보겠습니다.
std::views::split_when란 무엇인가요?
- std::views::split_when(rng, pred): 입력 범위 rng를 순회하면서, 인접한 두 원소 (a, b)를 비교할 때 조건자 pred(a, b)가 참을 반환하는 지점에서 새 부분 범위를 시작합니다.
- 즉, split_when은 split의 일반화된 형태로, 구분자를 고정된 값이 아닌 조건 로직으로 정의할 수 있는 강력한 도구입니다.
예를 들어, 문자열 범위를 처리할 때 단순히 공백 문자로 나누는 것이 아니라, 특정 패턴(예: 알파벳에서 숫자로 전환될 때)에서 나누는 등 훨씬 복잡한 조건에 따라 범위를 분할할 수 있습니다.
이전 버전에서는 어떻게 했나요?
C++20에서 std::views::split을 사용하면 특정 구분 값(예: 문자, 정수)을 기준으로 범위를 나눌 수 있었습니다. 하지만 "문자열이 대문자에서 소문자로 바뀌는 지점"처럼 동적인 조건으로 구분하려면 별도의 iterator 조작과 조건 체크 로직을 수동으로 구현해야 했습니다.
예제: 기존 방식(C++20까지)
#include <string_view>
#include <vector>
#include <iostream>
// 예: 알파벳에서 숫자로 바뀌는 지점에서 문자열을 분할하고 싶다면?
// C++20에서는 split은 고정 구분자만 가능하므로, 다음과 같이 수동 구현 필요:
std::vector<std::string_view> split_when(std::string_view sv) {
std::vector<std::string_view> result;
std::size_t start = 0;
for (std::size_t i = 1; i < sv.size(); ++i) {
bool prevAlpha = std::isalpha(static_cast<unsigned char>(sv[i-1]));
bool currDigit = std::isdigit(static_cast<unsigned char>(sv[i]));
if (prevAlpha && currDigit) {
result.push_back(sv.substr(start, i - start));
start = i;
}
}
result.push_back(sv.substr(start));
return result;
}
int main() {
auto parts = split_when("ABC123XYZ45");
// parts: {"ABC", "123XYZ", "45"}
for (auto p : parts) {
std::cout << p << '\n';
}
return 0;
}
- 문제점: 직접 인덱스 계산, 조건 체크, 부분 문자열 추출 로직 필요. 코드 장황하고 유지보수 어려움.
C++23의 std::views::split_when 사용 예제
#include <ranges>
#include <string_view>
#include <iostream>
int main() {
std::string_view sv = "ABC123XYZ45";
// 알파벳에서 숫자로 바뀌는 지점에서 split
auto chunks = sv | std::views::split_when([](char a, char b) {
bool prevAlpha = std::isalpha(static_cast<unsigned char>(a));
bool currDigit = std::isdigit(static_cast<unsigned char>(b));
return prevAlpha && currDigit;
});
for (auto subrange : chunks) {
// subrange는 부분 범위를 나타내며
// string_view로 변환하여 출력 가능
std::string_view part(&*subrange.begin(), std::ranges::distance(subrange));
std::cout << part << '\n';
}
// 출력:
// ABC
// 123XYZ
// 45
return 0;
}
- split_when을 사용하면 구분 로직을 람다 하나로 명확히 정의할 수 있어 코드가 간결해지고 가독성 향상.
어떻게 좋아졌나요?
- 조건 기반 분할 로직 단순화: 기존에는 인덱스 계산과 iterator 조작 필요, 이제 람다 하나로 조건 정의 가능.
- 가독성 향상: 분할 의도가 람다를 통해 명확히 드러나며, 파이프라인 형태로 다른 뷰와 쉽게 결합 가능.
- 범위 및 타입 유연성: 문자열뿐 아니라 모든 범위에 적용 가능하며, 조건자 로직 변경만으로 분할 패턴도 쉽게 수정 가능.
상세한 예제와 비교
다른 뷰와 조합
#include <ranges>
#include <string_view>
#include <iostream>
int main() {
std::string_view text = "AaAaBbBbCcC";
// 대문자에서 소문자로, 혹은 소문자에서 대문자로 바뀔 때 split
auto patterns = text
| std::views::split_when([](char a, char b) {
bool aUpper = std::isupper(static_cast<unsigned char>(a));
bool bUpper = std::isupper(static_cast<unsigned char>(b));
return aUpper != bUpper; // 대소문자가 변경되는 지점에서 split
})
| std::views::filter([](auto r) { return !r.empty(); }); // 빈 부분 제거
for (auto subrange : patterns) {
std::string_view part(&*subrange.begin(), std::ranges::distance(subrange));
std::cout << part << '\n';
}
// 출력 예: A, a, A, a, B, b, B, b, C, c, C
return 0;
}
- split_when 후 filter 등 다른 뷰 어댑터와 결합하여 더욱 정교한 데이터 파이프라인 구현 가능.
주의 사항
- 조건자 로직 주의: 인접한 두 원소 (a, b)를 전달받아 참/거짓을 반환하는 람다 필요. 이 로직이 잘못되면 의도치 않은 분할 발생.
- C++23 지원 여부: C++23 기능이므로, 해당 기능 지원 컴파일러와 표준 라이브러리 필요.
요약
C++23의 std::views::split_when은 구분 조건을 단순한 값이 아닌 동적인 조건자로 정의하여 범위를 분할하는 강력한 뷰 어댑터입니다. 이전에는 복잡한 iterator 조작과 조건 체크 로직을 수동으로 구현해야 했던 부분을, 이제 단 한 줄의 람다로 간결하게 표현할 수 있어 코드 가독성, 유지보수성, 생산성이 모두 향상됩니다. 다른 뷰 어댑터와도 쉽게 결합할 수 있어, 데이터 처리 파이프라인 구성에 큰 도움을 줍니다.
참고 자료:
'개발 이야기 > C++' 카테고리의 다른 글
[C++23 새기능 소개] std::views::stride (0) | 2024.12.09 |
---|---|
[C++23 새기능 소개] std::spanstream (0) | 2024.12.09 |
[C++23 새기능 소개] std::ranges::starts_with, std::ranges::ends_with, std::ranges::contains (0) | 2024.12.09 |
[C++23 새기능 소개] std::to_underlying (0) | 2024.12.09 |
[C++23 새기능 소개] std::views::slide (1) | 2024.12.09 |