C++20과 C++23을 활용한 “파이썬스러운” API 구현 #11: grouper, intersperse, flatten

여러분, 지난 글들에서 우리는 파이썬 itertools와 more-itertools의 다양한 기능을 C++20/23 문법으로 흉내내는 방법을 탐구해왔습니다. 이번에는 grouper, intersperse, flatten이라는 세 가지 기능을 살펴보며, C++로도 충분히 파이썬스러운 추상화를 구현할 수 있음을 확인해보겠습니다.

 

먼저 파이썬에서의 해당 API를 간단히 살펴볼게요. grouper, intersperse, flatten는 more-itertools나 itertools 관련 라이브러리로 자주 사용할 수 있는 패턴입니다.

from more_itertools import grouper, intersperse, flatten

# grouper(iterable, n, fillvalue=None)
# iterable을 n개씩 묶은 튜플을 반환. 마지막 그룹이 n보다 작으면 fillvalue로 채움.
for group in grouper([1,2,3,4,5], 2, fillvalue='X'):
    print(group)
# (1, 2)
# (3, 4)
# (5, 'X')

# intersperse(element, iterable)
# iterable의 원소 사이 사이에 element를 끼워넣는다.
for x in intersperse("X", ["a","b","c","d"]):
    print(x, end=' ')
# a X b X c X d

print()

# flatten(iterable_of_iterables)
# 중첩된 리스트/튜플 등을 1차원으로 펼친다. chain.from_iterable와 유사하지만
# flatten은 더 다양한 형태를 처리할 수도 있음.
data = [[1,2],[3,4],[5]]
for x in flatten(data):
    print(x, end=' ')
# 1 2 3 4 5

 

이제 이를 C++에서 구현해봅시다.

1. grouper 구현 (기본 형태)

grouper(iterable, n, fillvalue=None)는 이터러블을 길이 n짜리 묶음으로 잘라내며, 마지막 묶음이 n보다 짧으면 fillvalue로 채웁니다. 이전 글에서 batched와 비슷한 개념을 다룬 적 있지만, batched는 마지막이 짧으면 그대로 반환했던 반면, grouper는 빈 칸을 fillvalue로 채워넣는 차이가 있습니다.

#include <vector>
#include <iterator>
#include <optional>

template<typename Container, typename T>
class grouper_view {
public:
    grouper_view(Container& c, std::size_t n, T fillvalue)
        : c_(c), n_(n), fill_(fillvalue) {}

    class batch_iterator {
    public:
        using base_it = decltype(std::begin(std::declval<Container&>()));
        batch_iterator(base_it start, base_it end, std::size_t n, T fill)
            : start_(start), end_(end), n_(n), fill_(fill) {}

        batch_iterator& operator++() {
            std::size_t count = 0;
            while (start_ != end_ && count < n_) {
                ++start_;
                ++count;
            }
            return *this;
        }

        bool operator!=(const batch_iterator& other) const {
            return start_ != other.start_;
        }

        auto operator*() const {
            std::vector<T> group;
            auto batch_start = start_;
            // operator*는 for-range 루프에서 *를 호출한 뒤 ++가 호출되는 순서로 진행되므로,
            // 여기서 start_는 현재 배치의 시작점이다.
            // 정확히는, begin()에서 받은 iterator가 처음 batch 시작점이고,
            // for-range 루프 첫 it에서 operator*는 현재 batch, operator++은 다음 batch 시작점으로 진행.
            
            // n개 또는 end까지 읽기
            std::size_t count = 0;
            auto it = batch_start;
            while (it != end_ && count < n_) {
                group.push_back(*it);
                ++it;
                ++count;
            }
            // 부족하면 fill로 채우기
            while (count < n_) {
                group.push_back(fill_);
                ++count;
            }
            return group;
        }

    private:
        base_it start_;
        base_it end_;
        std::size_t n_;
        T fill_;
    };

    auto begin() {
        return batch_iterator(std::begin(c_), std::end(c_), n_, fill_);
    }
    auto end() {
        return batch_iterator(std::end(c_), std::end(c_), n_, fill_);
    }

private:
    Container& c_;
    std::size_t n_;
    T fill_;
};

template<typename Container, typename T>
auto grouper_func(Container& c, std::size_t n, T fillvalue) {
    return grouper_view<Container, T>(c, n, fillvalue);
}

 

사용 예제:

std::vector<int> nums = {1,2,3,4,5};
for (auto group : grouper_func(nums, 2, 999)) {
    for (auto x : group) std::cout << x << " ";
    std::cout << "\n";
}
// 1 2
// 3 4
// 5 999

2. intersperse 구현 (기본 형태)

intersperse(element, iterable)는 iterable 원소 사이에 element를 끼워넣습니다. 원소 사이사이에 sep를 놓으려면, 원소 하나를 출력한 뒤 sep, 그 다음 원소, 다시 sep ... 의 패턴을 생각할 수 있습니다.

template<typename Container, typename T>
class intersperse_view {
public:
    intersperse_view(Container& c, T sep) : c_(c), sep_(sep) {}

    class iterator {
    public:
        using base_it = decltype(std::begin(std::declval<Container&>()));

        iterator(base_it current, base_it end, T sep, bool sep_mode)
            : current_(current), end_(end), sep_(sep), sep_mode_(sep_mode) {}

        iterator& operator++() {
            if (!sep_mode_) {
                // 현재 원소 출력 후 sep 모드로
                sep_mode_ = true;
            } else {
                // sep 출력 후 다음 원소로 이동
                ++current_;
                sep_mode_ = false;
            }
            return *this;
        }

        bool operator!=(const iterator& other) const {
            return current_ != other.current_;
        }

        auto operator*() const {
            if (!sep_mode_) {
                return *current_;
            } else {
                return sep_;
            }
        }

    private:
        base_it current_;
        base_it end_;
        T sep_;
        bool sep_mode_; // false: 원소 모드, true: sep 모드
    };

    auto begin() {
        auto b = std::begin(c_);
        auto e = std::end(c_);
        if (b == e) {
            return iterator(e,e,sep_,false); // 빈 컨테이너면 아무 것도 출력 없음
        }
        return iterator(b,e,sep_,false);
    }
    auto end() {
        return iterator(std::end(c_), std::end(c_), sep_, false);
    }

private:
    Container& c_;
    T sep_;
};

template<typename Container, typename T>
auto intersperse_func(Container& c, T sep) {
    return intersperse_view<Container, T>(c, sep);
}

 

사용 예제:

std::vector<std::string> vec = {"a","b","c","d"};
for (auto x : intersperse_func(vec, std::string("X"))) {
    std::cout << x << " "; 
}
// a X b X c X d

3. flatten 구현 (기본 형태)

flatten(iterable_of_iterables)는 중첩 이터러블을 1차원으로 펼칩니다. 이전 글에서 chain.from_iterable를 구현한 것과 거의 동일합니다.

template<typename OuterContainer>
class flatten_view {
public:
    flatten_view(OuterContainer& outer) : outer_(outer) {}

    class iterator {
    public:
        using outer_it = decltype(std::begin(std::declval<OuterContainer&>()));
        using inner_container_type = typename std::remove_reference_t<decltype(*std::declval<outer_it>())>;
        using inner_it = decltype(std::begin(std::declval<inner_container_type&>()));

        iterator(outer_it out_it, outer_it out_end)
            : out_it_(out_it), out_end_(out_end) {
            advance_to_valid_inner();
        }

        iterator& operator++() {
            if (out_it_ == out_end_) return *this;
            ++in_it_;
            if (in_it_ == in_end_) {
                ++out_it_;
                advance_to_valid_inner();
            }
            return *this;
        }

        bool operator!=(const iterator& other) const {
            return out_it_ != other.out_it_ || in_it_ != other.in_it_;
        }

        auto operator*() const {
            return *in_it_;
        }

    private:
        void advance_to_valid_inner() {
            while (out_it_ != out_end_) {
                auto& inner = *out_it_;
                in_it_ = std::begin(inner);
                in_end_ = std::end(inner);
                if (in_it_ != in_end_) break;
                ++out_it_;
            }
        }

        outer_it out_it_, out_end_;
        inner_it in_it_, in_end_;
    };

    auto begin() {
        return iterator(std::begin(outer_), std::end(outer_));
    }
    auto end() {
        return iterator(std::end(outer_), std::end(outer_));
    }

private:
    OuterContainer& outer_;
};

template<typename OuterContainer>
auto flatten_func(OuterContainer& outer) {
    return flatten_view<OuterContainer>(outer);
}

 

사용 예제:

std::vector<std::vector<int>> nested = {{1,2},{3,4},{5}};
for (auto x : flatten_func(nested)) {
    std::cout << x << " "; // 1 2 3 4 5
}
std::cout << "\n";

정교한(또는 확장된) 구현: C++20 Ranges와 Concepts 활용

  • grouper: Ranges 파이프라인에 쉽게 녹일 수 있고, Concepts로 타입 검증 가능.
  • intersperse: range-v3에 views::intersperse가 이미 구현되어 있어서 더 간단히 가능.
  • flatten: view::join(range-v3)을 이용하면 한 줄로 flatten 구현 가능.

성능과 메모리 측면

  • grouper: 매 batch마다 vector 생성, fillvalue 채우기.
  • intersperse: sep 반환 시 별도 메모리 할당 없음.
  • flatten: lazy evaluation으로 O(1) 추가 메모리, 필요할 때만 inner 이터레이션.

관련 링크 및 리소스

마무리

이번 글에서 다룬 grouper, intersperse, flatten는 파이썬의 강력한 이터레이션 패턴을 C++로 옮기는 또 다른 예시입니다. 이렇게 직접 구현해보면 파이썬이 얼마나 간결한 문법을 갖추고 있는지, 그리고 C++20/23 이후로 얼마나 표현력이 강화되었는지 새삼 느끼게 됩니다.

 

여러분도 이 아이디어를 확장해, 프로젝트에서 더욱 우아한 C++ 코드를 작성해보세요. 현대 C++과 파이썬 itertools 패턴의 결합은 생각보다 많은 가능성을 열어줍니다.

반응형