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

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 조작과 조건 체크 로직을 수동으로 구현해야 했던 부분을, 이제 단 한 줄의 람다로 간결하게 표현할 수 있어 코드 가독성, 유지보수성, 생산성이 모두 향상됩니다. 다른 뷰 어댑터와도 쉽게 결합할 수 있어, 데이터 처리 파이프라인 구성에 큰 도움을 줍니다.

 

참고 자료:

반응형