C++20과 C++23을 활용한 “파이썬스러운” API 구현 #4: itertools 스타일 – chain, takewhile, dropwhile

앞선 글들에서 우리는 파이썬의 range, enumerate, zip, map, filter와 같은 편리한 함수형 API를 C++20/23 문법과 Ranges 라이브러리 등을 통해 “파이썬스럽게” 구현하는 방법을 살펴보았습니다. 이번 글에서는 한 단계 더 나아가, 파이썬의 itertools 라이브러리가 제공하는 몇 가지 대표적인 툴을 C++ 스타일로 흉내 내보려 합니다.

 

itertools는 파이썬에서 반복 가능한(iterable) 시퀀스를 다루는 데 매우 유용한 툴셋을 제공합니다. 그중에서도 chain, takewhile, dropwhile는 다양한 컨테이너나 이터러블을 직관적이고 유연하게 다룰 수 있게 해줍니다.

 

글의 구성은 다음과 같습니다:

  1. 일반적인 C++ 구현 (Before): 파이썬 chain, takewhile, dropwhile 없이 비슷한 로직을 구현하려면?
  2. 단순한 Python 같은 C++ 구현 (After: 첫 단추): 간단한 view를 만들어 itertools 스타일의 API를 흉내 내는 기본적인 방법.
  3. 정교한(또는 확장된) 구현: C++20 Ranges, Concepts, 파이프 연산자 등을 활용해 보다 탄탄하고 범용적으로 구현하는 방법.

마지막에는 관련된 리소스와 링크를 정리한 섹션도 제공합니다.

1. 일반적인 C++에서의 구현 (Before)

chain은 여러 컨테이너를 하나로 이어 붙여서 순회하는 기능을 합니다. 이를 C++에서 하려면 보통 수동으로 인덱스를 관리하거나, 별도의 loop를 중첩하는 식으로 처리하곤 합니다.

#include <vector>
#include <iostream>
#include <string>

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

    // chain: v1 뒤에 v2를 이어붙이는 느낌으로 순회
    for (auto x : v1) std::cout << x << " ";
    for (auto x : v2) std::cout << x << " ";
    std::cout << "\n"; 
    // 출력: 1 2 3 4 5 6

    // takewhile: 조건이 참인 동안만 순회
    // 예를 들어, v2에서 x<6인 동안만 출력
    for (auto x : v2) {
        if (x < 6) std::cout << x << " "; else break;
    }
    std::cout << "\n"; // 출력: 4 5

    // dropwhile: 조건이 참인 동안은 건너뛰고, 조건이 거짓이 되는 시점부터 출력
    // v2에서 x<5인 동안은 스킵, 이후부터 출력
    bool still_drop = true;
    for (auto x : v2) {
        if (still_drop && x < 5) continue;
        still_drop = false;
        std::cout << x << " ";
    }
    std::cout << "\n"; // 출력: 5 6
}

이 방식은 명시적이지만, 파이썬처럼 “itertools.chain(v1, v2)”, “itertools.takewhile(cond, v2)”, “itertools.dropwhile(cond, v2)”라고 쓰는 것보다 장황합니다. C++20 이후 기능을 활용하면 보다 깔끔한 abstraction을 만들 수 있습니다.

2. 단순한 Python 같은 C++ 구현 (After: 첫 단추)

여기서는 파이썬 itertools와 유사하게 작동하는 간단한 view를 직접 만들어보겠습니다. 매우 단순화된 형태이며, C++17 스타일로도 구현 가능한 수준을 먼저 보여줍니다.

chain 구현 (2개 컨테이너)

template<typename C1, typename C2>
class chain_view {
public:
    chain_view(C1& c1, C2& c2) : c1_(c1), c2_(c2) {}

    class iterator {
    public:
        using it1_type = decltype(std::begin(std::declval<C1&>()));
        using it2_type = decltype(std::begin(std::declval<C2&>()));

        iterator(it1_type it1, it1_type end1, it2_type it2, it2_type end2, bool first)
            : it1_(it1), end1_(end1), it2_(it2), end2_(end2), first_(first) {}

        iterator& operator++() {
            if (first_) {
                ++it1_;
                if (it1_ == end1_) {
                    first_ = false;
                }
            } else {
                ++it2_;
            }
            return *this;
        }

        auto operator*() const {
            return first_ ? *it1_ : *it2_;
        }

        bool operator!=(const iterator& other) const {
            if (first_ != other.first_) return true;
            return first_ ? (it1_ != other.it1_) : (it2_ != other.it2_);
        }

    private:
        it1_type it1_, end1_;
        it2_type it2_, end2_;
        bool first_;
    };

    auto begin() {
        return iterator(std::begin(c1_), std::end(c1_), std::begin(c2_), std::end(c2_), true);
    }
    auto end() {
        return iterator(std::end(c1_), std::end(c1_), std::end(c2_), std::end(c2_), false);
    }

private:
    C1& c1_;
    C2& c2_;
};

template<typename C1, typename C2>
auto chain(C1& c1, C2& c2) {
    return chain_view<C1,C2>(c1, c2);
}

사용 예제:

std::vector<int> v1 = {1,2,3};
std::vector<int> v2 = {4,5,6};

for (auto x : chain(v1, v2)) {
    std::cout << x << " "; // 1 2 3 4 5 6
}
std::cout << "\n";

takewhile, dropwhile 구현

template<typename Container, typename Pred>
class takewhile_view {
public:
    takewhile_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), done_(false) {
            if (it_ == end_) done_ = true;
            else if (!p_(*it_)) done_ = true;
        }
        iterator& operator++() {
            ++it_;
            if (it_ == end_ || !p_(*it_)) done_ = true;
            return *this;
        }
        auto operator*() const { return *it_; }
        bool operator!=(const iterator& other) const {
            return done_ != other.done_ || it_ != other.it_;
        }
    private:
        base_it it_, end_;
        Pred p_;
        bool done_;
    };

    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 takewhile_func(Container& c, Pred p) {
    return takewhile_view<Container,Pred>(c, p);
}

template<typename Container, typename Pred>
class dropwhile_view {
public:
    dropwhile_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), dropping_(true) {
            advance_past_drop();
        }
        iterator& operator++() {
            ++it_;
            return *this;
        }
        auto operator*() const { return *it_; }
        bool operator!=(const iterator& other) const {
            return it_ != other.it_;
        }

    private:
        void advance_past_drop() {
            while (it_ != end_ && dropping_) {
                if (p_(*it_)) ++it_;
                else dropping_ = false;
            }
        }

        base_it it_, end_;
        Pred p_;
        bool dropping_;
    };

    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 dropwhile_func(Container& c, Pred p) {
    return dropwhile_view<Container,Pred>(c, p);
}

사용 예제:

std::vector<int> v = {1,2,3,4,5,6};
for (auto x : takewhile_func(v, [](int x){return x<4;})) {
    std::cout << x << " "; // 1 2 3
}
std::cout << "\n";

for (auto x : dropwhile_func(v, [](int x){return x<4;})) {
    std::cout << x << " "; // 4 5 6
}
std::cout << "\n";

이렇게 구현하면, 파이썬 itertools 스타일의 동작을 어느 정도 흉내낼 수 있습니다. 비록 완벽히 같지는 않지만, 표현력을 크게 개선할 수 있습니다.

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

C++20에는 std::views 네임스페이스 아래 다양한 뷰들이 존재하고, 이들과 조합하면 itertools와 유사한 패턴을 훨씬 깔끔하게 만들 수 있습니다. 아쉽게도 표준 라이브러리에 chain이나 takewhile, dropwhile는 아직 정식 포함되지 않았습니다. 하지만 이런 기능을 제공하는 오픈소스 라이브러리나 C++23 이후의 확장을 기대할 수 있습니다.

 

또한 별도의 라이브러리를 사용하거나, C++23에 도입된 std::views::drop_while (C++23 표준 초안 기준) 등과 조합하면 훨씬 직관적으로 코드를 작성할 수 있습니다. chain에 해당하는 기능은 std::ranges::join_view를 응용하거나, 여러 컨테이너를 하나로 이어붙일 수 있는 커스텀 뷰를 만들 수 있습니다.

 

컨셉(Concepts)을 이용하면, 입력이 실제 Range인지, Predicate가 bool을 반환하는지 등 여러 조건을 컴파일 타임에 검사할 수 있어, 더욱 안정적이고 타입 안전한 구현을 할 수 있습니다.

잠깐 살펴보기: join_view를 이용한 chain 유사 동작

C++20 Ranges에는 std::views::join이 존재합니다. 이는 컨테이너의 컨테이너(예: std::vector<std::vector<int>>)를 하나의 평면화된 시퀀스로 펼쳐줍니다. chain을 정확히 대체하지 못하지만, 비슷한 패턴을 만들 수 있습니다.

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

int main() {
    std::vector<std::vector<int>> vv = {{1,2,3}, {4,5,6}};
    for (auto x : vv | std::views::join) {
        std::cout << x << " "; // 1 2 3 4 5 6
    }
    std::cout << "\n";
}

여러 개의 컨테이너를 하나의 벡터로 묶고, join을 적용하면 chain에 가까운 결과를 얻을 수 있습니다.

takewhile, dropwhile C++23 지원 기대

C++23 표준에서는 std::views::take_while, std::views::drop_while가 정식으로 추가되었습니다. 이를 활용하면 굳이 커스텀 구현 없이 파이썬 itertools의 takewhile, dropwhile에 가까운 동작을 바로 사용할 수 있습니다.

// C++23(초안) 스타일 예시 (개념적 코드)
#include <ranges>
#include <iostream>
#include <vector>

int main() {
    std::vector<int> v = {1,2,3,4,5};
    for (auto x : v | std::views::take_while([](int x){return x<4;})) {
        std::cout << x << " "; // 1 2 3
    }
    std::cout << "\n";
}

성능과 메모리 측면

  • 이터레이터 기반 뷰는 lazy evaluation 덕분에 추가 메모리 할당 없이 동작합니다.
  • 단순 반복자 증가, 조건 검사 정도의 비용만 발생하므로, 최적화된 빌드에서는 기본 for 루프와 큰 성능 차이가 나지 않습니다.
  • C++20/23 표준 뷰는 컴파일러 최적화를 적극 유도할 수 있어, 성능 측면에서도 유리한 경우가 많습니다.

관련 링크 및 리소스

마무리

이번 글에서는 파이썬 itertools의 핵심 기능 중 일부인 chain, takewhile, dropwhile를 C++에서 구현하는 방법을 살펴봤습니다. 단순한 직접 구현에서 출발해 Ranges, Concepts, 그리고 C++23 기능까지 활용하면서 점점 더 “파이썬스럽고” 정교한 API를 만들 수 있다는 점을 확인했습니다. C++20 이후의 기능을 잘 활용하면, 파이썬 수준의 선언적이고 직관적인 반복처리 로직을 C++에서도 구현할 수 있게 됩니다.

 

반응형