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

C++23에서는 범위(Range) 라이브러리에 다양한 뷰(View) 어댑터가 추가되어, 데이터 처리 파이프라인을 더욱 간결하고 표현력 있게 만들 수 있습니다. 그중 하나인 std::views::slide는 입력 범위에 대해 슬라이딩 윈도우(sliding window)를 적용하는 뷰 어댑터입니다. 이를 통해 고정 크기의 창(window)을 범위를 따라 이동시키면서 각 위치에서의 부분 범위를 쉽게 얻을 수 있습니다.

이번 글에서는 std::views::slide의 개념과 사용법, 그리고 이전 방식과 비교하여 어떠한 개선점을 제공하는지 알아보겠습니다.

 

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

std::views::slide(n)는 입력 범위에 대해 길이 n의 윈도우를 순회하면서, 각 위치에서 길이 n짜리의 부분 범위를 뷰로 제공하는 어댑터입니다. 창(window)은 1칸씩 전진하며, 각 전진 시마다 새로운 부분 범위(서브레인지)를 반환합니다.

 

예를 들어, [1,2,3,4,5] 범위에 slide(3)를 적용하면 다음과 같은 부분 범위를 순차적으로 제공합니다.

  • 첫 번째 위치: [1,2,3]
  • 두 번째 위치: [2,3,4]
  • 세 번째 위치: [3,4,5]

이렇게 연속적인 요소를 일정 크기 단위로 슬라이딩하면서 검사하는 패턴은 통계, 신호 처리, 머신러닝(윈도우 평균, 이동 평균), 문자열 처리(부분 문자열 검색), 게임 로직(인접 3개 요소 비교) 등 다양한 분야에서 유용하게 활용할 수 있습니다.

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

C++20까지는 슬라이딩 윈도우 로직을 구현하기 위해 다음과 같은 방법이 필요했습니다.

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

#include <vector>
#include <iostream>

int main() {
    std::vector<int> vec = {1,2,3,4,5};
    std::size_t window_size = 3;

    // 인덱스를 활용한 수동 구현
    for (std::size_t start = 0; start + window_size <= vec.size(); ++start) {
        for (std::size_t i = start; i < start + window_size; ++i) {
            std::cout << vec[i] << ' ';
        }
        std::cout << '\n';
    }

    return 0;
}
  • 문제점: 인덱스 계산, boundary 체크, 각 윈도우를 서브컨테이너로 추출하는 과정 등이 번거롭고, 로직이 장황합니다.
  • 파이프라인 형태로 다른 범위 어댑터와 결합하기도 어렵습니다.

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

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

int main() {
    std::vector<int> vec = {1,2,3,4,5};

    // slide(3)을 적용하여 길이 3의 윈도우 이동
    auto windows = vec | std::views::slide(3);

    for (auto window : windows) {
        for (int x : window) {
            std::cout << x << ' ';
        }
        std::cout << '\n';
    }
    // 출력:
    // 1 2 3
    // 2 3 4
    // 3 4 5

    return 0;
}
  • 코드가 훨씬 간결해지고 의도가 명확해졌습니다.
  • windows는 [[1,2,3],[2,3,4],[3,4,5]]와 같은 부분 범위들의 뷰를 제공합니다.

어떻게 좋아졌나요?

  • 코드 간결성: 인덱스 계산이나 boundary 처리 등 보일러플레이트 코드가 필요 없어집니다.
  • 가독성 향상: slide를 보는 순간 슬라이딩 윈도우 로직을 직관적으로 파악할 수 있습니다.
  • 함수형 프로그래밍 스타일: 다른 범위 어댑터(filter, transform, take, join 등)와 결합이 쉬워, 복잡한 데이터 파이프라인도 한 줄에 표현 가능.
  • 유연성: 윈도우 크기만 바꾸면 손쉽게 다른 단위로 처리 가능.

상세한 예제와 비교

다른 뷰와의 조합

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

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

    // slide(3) 후 각 윈도우의 합계를 구하는 예제
    // zip_transform과 함께 사용 가능하지만 단순 transform 사용 예시
    auto window_sums = data
        | std::views::slide(3)
        | std::views::transform([](auto window) {
            int sum = 0;
            for (int x : window) sum += x;
            return sum;
        });

    for (int s : window_sums) {
        std::cout << s << ' ';
    }
    // 출력: 6 (1+2+3), 9 (2+3+4), 12 (3+4+5), 15 (4+5+6), 18 (5+6+7)

    return 0;
}
  • slide를 통해 얻은 각 윈도우를 transform을 통해 합계로 변환하는 로직을 쉽게 구현.

주의 사항

  • 윈도우 크기 관리: slide(n)에서 n이 범위보다 크면 마지막 부분 윈도우는 줄어든 형태로 나옵니다. 이 점을 알고 로직을 설계해야 합니다.
  • 컴파일러 및 라이브러리 지원: C++23 기능이므로, 해당 기능을 지원하는 컴파일러와 표준 라이브러리가 필요합니다.
  • 성능 고려: 슬라이딩 윈도우 처리 시, 큰 데이터에 대해서는 성능 최적화를 고려할 수 있습니다. 그러나 범위 뷰 자체는 lazy evaluation을 통해 불필요한 계산을 줄여줍니다.

요약

C++23의 std::views::slide는 범위에 슬라이딩 윈도우를 적용하는 기능을 제공함으로써 데이터 처리 파이프라인을 한층 강화합니다. 인덱스 기반 접근 없이 간결한 코드로 윈도우 단위의 처리 로직을 구현할 수 있으며, 다른 범위 어댑터와의 조합으로 복잡한 데이터 처리 파이프라인을 손쉽게 구성할 수 있습니다. 이를 통해 가독성, 유지보수성, 코드 품질과 생산성이 크게 향상됩니다.

 

참고 자료:

반응형