C++20과 C++23을 활용한 “파이썬스러운” API 구현 #8: compress, filterfalse, starmap, combinations_with_replacement

안녕하세요! 지난 글들에서 우리는 파이썬 itertools의 다양한 기능들을 C++20/23 문법과 Ranges 라이브러리, 그리고 다양한 기법을 활용해 “파이썬스러운” 스타일로 구현하는 시도를 해왔습니다. 이제 꽤 많은 기능을 다뤘죠. 이번에는 아직 소개하지 않았던 네 가지를 살펴보겠습니다:

  • compress(data, selectors): data와 selectors를 나란히 순회하면서 selectors가 참일 때만 data의 원소를 내보냅니다.
  • filterfalse(predicate, iterable): filter의 반대 버전으로, predicate가 거짓일 때만 원소를 내보냅니다.
  • starmap(function, iterable_of_iterables): 각 원소가 튜플(혹은 리스트)인 이터러블을 받아, 해당 원소를 unpack한 후 function에 적용한 결과를 반환합니다.
  • combinations_with_replacement(iterable, r): combinations와 비슷하지만, 같은 원소를 여러 번 선택할 수 있는 조합을 생성합니다. 예를 들어 [1,2,3]에서 r=2로 combinations_with_replacement를 구하면 (1,1), (1,2), (1,3), (2,2), (2,3), (3,3)가 나옵니다.

이 함수들 역시 파이썬만큼 깔끔하게는 어렵지만, 기본 아이디어를 간단한 뷰(view) 형태로 구현해보겠습니다.

1. compress 구현 (기본 형태)

compress(data, selectors)는 data와 selectors를 동시에 순회하면서, selectors[i]가 true라면 data[i]를 yield합니다.

template<typename DataContainer, typename SelectContainer>
class compress_view {
public:
    compress_view(DataContainer& data, SelectContainer& sel)
        : data_(data), sel_(sel) {}

    class iterator {
    public:
        using data_it = decltype(std::begin(std::declval<DataContainer&>()));
        using sel_it = decltype(std::begin(std::declval<SelectContainer&>()));

        iterator(data_it dit, data_it dend, sel_it sit, sel_it send)
            : dit_(dit), dend_(dend), sit_(sit), send_(send) {
            advance_to_valid();
        }

        iterator& operator++() {
            ++dit_; ++sit_;
            advance_to_valid();
            return *this;
        }

        bool operator!=(const iterator& other) const { return dit_ != other.dit_ || sit_ != other.sit_; }
        auto operator*() const { return *dit_; }

    private:
        void advance_to_valid() {
            while (dit_ != dend_ && sit_ != send_ && !(*sit_)) {
                ++dit_; ++sit_;
            }
            if (dit_ == dend_ || sit_ == send_) {
                // 끝을 맞추기 위해
                dit_ = dend_;
                sit_ = send_;
            }
        }

        data_it dit_, dend_;
        sel_it sit_, send_;
    };

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

private:
    DataContainer& data_;
    SelectContainer& sel_;
};

template<typename DataContainer, typename SelectContainer>
auto compress_func(DataContainer& data, SelectContainer& sel) {
    return compress_view<DataContainer, SelectContainer>(data, sel);
}

사용 예제:

#include <vector>
#include <iostream>

int main() {
    std::vector<int> data = {10,20,30,40};
    std::vector<bool> selectors = {true, false, true, false};

    for (auto x : compress_func(data, selectors)) {
        std::cout << x << " "; // 10 30
    }
    std::cout << "\n";
}

2. filterfalse 구현 (기본 형태)

filterfalse는 파이썬의 itertools.filterfalse처럼 predicate를 만족하지 않는 원소들만 내보냅니다. filter의 반대 버전이죠.

template<typename Container, typename Pred>
class filterfalse_view {
public:
    filterfalse_view(Container& c, Pred p) : c_(c), p_(p) {}

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

        iterator(base_it it, base_it end, Pred p)
            : it_(it), end_(end), p_(p) {
            advance_to_valid();
        }

        iterator& operator++() {
            ++it_;
            advance_to_valid();
            return *this;
        }

        bool operator!=(const iterator& other) const { return it_ != other.it_; }
        auto operator*() const { return *it_; }

    private:
        void advance_to_valid() {
            while (it_ != end_ && p_(*it_)) {
                ++it_;
            }
        }

        base_it it_, end_;
        Pred p_;
    };

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

private:
    Container& c_;
    Pred p_;
};

template<typename Container, typename Pred>
auto filterfalse_func(Container& c, Pred p) {
    return filterfalse_view<Container, Pred>(c, p);
}

사용 예제:

std::vector<int> arr = {1,2,3,4,5,6};
for (auto x : filterfalse_func(arr, [](int x){return x%2==0;})) {
    std::cout << x << " "; // 짝수가 아닌 원소 => 1 3 5
}
std::cout << "\n";

3. starmap 구현 (기본 형태)

starmap(function, iterable_of_iterables)는 각 원소가 (tuple, list) 형태이고, 그 원소를 unpack하여 함수에 적용한 결과를 반환합니다. C++에서는 tuple을 받아서 std::apply로 함수에 인자를 펼치는 식으로 구현할 수 있습니다.

#include <tuple>
#include <utility>
#include <functional>

template<typename Container, typename Func>
class starmap_view {
public:
    starmap_view(Container& c, Func f) : c_(c), f_(f) {}

    class iterator {
    public:
        using base_it = decltype(std::begin(std::declval<Container&>()));
        using value_type = decltype(*std::declval<base_it>());
        // value_type이 tuple이라고 가정 (또는 pair)
        // 만약 다양한 형태를 처리하려면 concept나 SFINAE 필요

        iterator(base_it it, base_it end, Func f)
            : it_(it), end_(end), f_(f) {}

        iterator& operator++() {
            ++it_;
            return *this;
        }

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

        auto operator*() const {
            return std::apply(f_, *it_);
        }

    private:
        base_it it_, end_;
        Func f_;
    };

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

private:
    Container& c_;
    Func f_;
};

template<typename Container, typename Func>
auto starmap_func(Container& c, Func f) {
    return starmap_view<Container, Func>(c, f);
}

사용 예제:

std::vector<std::tuple<int,int>> pairs = {{1,10},{2,20},{3,30}};
for (auto x : starmap_func(pairs, [](int a, int b){return a+b;})) {
    std::cout << x << " "; // 11 22 33
}
std::cout << "\n";

4. combinations_with_replacement 구현 (기본 형태)

combinations_with_replacement는 combinations와 비슷하지만, 같은 원소를 여러 번 뽑을 수 있습니다(단 non-decreasing order로).

template<typename Container>
class combinations_with_replacement_view {
public:
    combinations_with_replacement_view(const Container& c, std::size_t r)
        : c_(c), r_(r) {
        sorted_.insert(sorted_.end(), c_.begin(), c_.end());
        std::sort(sorted_.begin(), sorted_.end());
        // 초기 상태는 모두 첫 원소로 시작
        indices_.assign(r_, 0);
        end_ = (r_ == 0 || c_.empty());
    }

    class iterator {
    public:
        iterator(combinations_with_replacement_view* parent, bool end)
            : parent_(parent), end_(end) {}

        iterator& operator++() {
            if (!next_comb()) {
                end_ = true;
            }
            return *this;
        }

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

        auto operator*() const {
            std::vector<typename Container::value_type> comb;
            for (auto i : parent_->indices_) {
                comb.push_back(parent_->sorted_[i]);
            }
            return comb;
        }

    private:
        bool next_comb() {
            // combinations_with_replacement 다음 조합 찾기
            // ex: indices = [0,0,1], r=3, n=길이
            std::size_t i = parent_->r_;
            while (i > 0) {
                --i;
                if (parent_->indices_[i] < parent_->sorted_.size()-1) {
                    std::size_t val = parent_->indices_[i]+1;
                    for (std::size_t j = i; j < parent_->r_; ++j) {
                        parent_->indices_[j] = val; 
                    }
                    return true;
                }
            }
            return false;
        }

        combinations_with_replacement_view* parent_;
        bool end_;
    };

    auto begin() {
        if (r_ == 0 || c_.empty()) {
            return iterator(this, true); // 빈 경우 바로 종료
        }
        return iterator(this, !end_);
    }
    auto end() {
        return iterator(this, true);
    }

private:
    const Container& c_;
    std::size_t r_;
    std::vector<typename Container::value_type> sorted_;
    std::vector<std::size_t> indices_;
    bool end_;
};

template<typename Container>
auto combinations_with_replacement_func(const Container& c, std::size_t r) {
    return combinations_with_replacement_view<Container>(c, r);
}

사용 예제:

std::vector<int> vals = {1,2,3};
for (auto comb : combinations_with_replacement_func(vals, 2)) {
    for (auto x : comb) std::cout << x << " ";
    std::cout << "\n";
}
// (1,1), (1,2), (1,3), (2,2), (2,3), (3,3)

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

C++20 Ranges와 Concepts를 적용하면 다음과 같은 개선이 가능해집니다.

  • Concepts: 입력 컨테이너가 forward_range인지, sortable한지 등을 제약해 더욱 안전하고 타입 친화적인 구현.
  • Range 기반 compose: data | compress_func(selectors) 식으로 파이프라인 연결, data | filterfalse_func(...) | transform(...) 형태로 다른 뷰들과 연계하기.
  • Tuples과 apply: starmap 부분에서 std::apply를 사용했는데, 이 부분을 Concepts나 meta-programming을 통해 더 일반화할 수 있습니다.
  • combinations_with_replacement: 좀 더 lazy하고 효율적인 구현을 할 수 있으며, range-v3 같은 라이브러리와 결합하면 C++ 코드를 대폭 간결화할 수 있습니다.

성능과 메모리 측면

  • compress, filterfalse: 추가 메모리 할당 없이 조건부로 원소를 거르는 lazy한 접근 가능.
  • starmap: tuple을 그대로 참조하거나, 값 반환 시 복사가 일어날 수 있음. 하지만 대체로 성능 부담은 크지 않습니다.
  • combinations_with_replacement: 현재 예제는 indices_ 배열을 매번 업데이트하며 lazy하게 조합을 생성합니다. 메모리 사용은 r 길이에 비례하는 정도이며, 모든 조합을 한 번에 생성하지 않으므로 무리한 메모리 사용은 없습니다.

관련 링크 및 리소스

마무리

이번 글에서는 compress, filterfalse, starmap, combinations_with_replacement를 C++20/23 스타일로 흉내 내봤습니다. 각 기능을 통해 파이썬 itertools의 풍부한 표현력을 C++에 어떻게 녹일 수 있는지 확인할 수 있었습니다. C++20 Ranges, Concepts, 그리고 코루틴을 비롯한 현대적 기능들이 적극적으로 활용되면, 앞으로 더 “파이썬스럽고” 간결한 C++ 코드를 짤 수 있는 기반이 더욱 탄탄해질 것입니다.

 

이제까지 여러 itertools 기능을 다뤄왔으니, 여러분도 프로젝트나 실험 코드를 통해 이런 기법들을 활용해보시면 어떨까요?

반응형