우리가 지금까지 다룬 파이썬 itertools 함수들 외에도, 파이썬에서 제공하는 편리한 반복 툴이 여전히 많이 남아 있습니다. 이번 글에서는 그중에서도 자주 쓰이는 세 가지를 골라봤습니다.
먼저, 파이썬으로 몇 가지 예제를 보여드릴게요:
from itertools import count, zip_longest, pairwise
# count는 start부터 step 간격으로 무한히 증가하는 숫자를 생성
for i in count(10, 2):
if i > 20:
break
print(i, end=' ')
# 출력: 10 12 14 16 18 20
# zip_longest는 가장 긴 이터러블이 끝날 때까지 묶음을 만들고, 부족한 곳은 fillvalue로 채워줌
list_a = [1, 2, 3]
list_b = ['a', 'b']
for x, y in zip_longest(list_a, list_b, fillvalue='X'):
print((x, y))
# 출력:
# (1, 'a')
# (2, 'b')
# (3, 'X') # b가 끝났으니 'X'로 채움
# pairwise는 연속된 원소들로 이루어진 튜플을 만듦 (Python 3.10+)
# 예: [10,20,30] -> (10,20), (20,30)
for p in pairwise([10,20,30,40]):
print(p)
# 출력:
# (10, 20)
# (20, 30)
# (30, 40)
이제 이 기능들을 C++20/23을 활용해 흉내 내보는 방법을 살펴보겠습니다.
1. count 구현 (기본 형태)
count(start, step)는 주어진 start부터 step 간격으로 무한히 증가하는 정수 스트림을 만듭니다. 파이썬에서는 무한 시퀀스를 반환하지만, C++에서는 실제 무한을 처리할 수 없으니, 필요할 때 break로 끊거나 특정 조건을 두어야 합니다.
template<typename T>
class count_view {
public:
count_view(T start, T step = 1) : current_(start), step_(step) {}
class iterator {
public:
iterator(T val, T step, bool end=false)
: val_(val), step_(step), end_(end) {}
iterator& operator++() {
val_ += step_;
return *this;
}
// 무한 시퀀스를 표현하기 위해 end 조건을 안쓰는 트릭: end를 가짜로 둠
bool operator!=(const iterator& other) const {
// 무한히 돌고 싶다면 end_를 안쓰면 되지만
// 여기서는 다른 iterator와 비교 시 항상 true 반환해서 끝나지 않도록 할 수도 있음.
// 일단 end_를 활용해 제한할 수도 있음.
return !end_;
}
T operator*() const { return val_; }
private:
T val_;
T step_;
bool end_;
};
auto begin() {
return iterator(current_, step_);
}
// end iterator를 무한히 돌려주지 않으려면, 특정 조건에서 end를 생성할 수도 있음.
// 여기서는 무한히 돌게 하기 위해 end를 같은 객체로 반환
auto end() {
return iterator(T{}, step_, true);
}
private:
T current_;
T step_;
};
template<typename T>
auto count_func(T start, T step = 1) {
return count_view<T>(start, step);
}
사용 예제:
for (auto x : count_func(10,2)) {
std::cout << x << " ";
if (x >= 20) break; // 10 12 14 16 18 20
}
std::cout << "\n";
2. zip_longest 구현 (기본 형태)
zip_longest는 여러 컨테이너를 동시 순회하되, 가장 긴 컨테이너가 끝날 때까지 진행하고, 짧은 컨테이너는 fillvalue로 채워줍니다. 여기서는 간단히 2개의 컨테이너만 처리하고, fillvalue를 하나만 받는 형태로 예를 들어보겠습니다(variadic template을 쓰면 더 많은 컨테이너도 가능).
template<typename C1, typename C2, typename T>
class zip_longest_view {
public:
zip_longest_view(C1& c1, C2& c2, T fill)
: c1_(c1), c2_(c2), fill_(fill) {}
class iterator {
public:
using it1 = decltype(std::begin(std::declval<C1&>()));
using it2 = decltype(std::begin(std::declval<C2&>()));
iterator(it1 i1, it1 e1, it2 i2, it2 e2, T fill)
: i1_(i1), e1_(e1), i2_(i2), e2_(e2), fill_(fill) {}
iterator& operator++() {
if (i1_ != e1_) ++i1_;
if (i2_ != e2_) ++i2_;
return *this;
}
bool operator!=(const iterator& other) const {
// 둘 다 끝나면 종료
bool end_self = (i1_ == e1_ && i2_ == e2_);
bool end_other = (other.i1_ == other.e1_ && other.i2_ == other.e2_);
return end_self != end_other;
}
auto operator*() const {
auto val1 = (i1_ != e1_) ? *i1_ : fill_;
auto val2 = (i2_ != e2_) ? *i2_ : fill_;
return std::pair<decltype(val1), decltype(val2)>(val1, val2);
}
private:
it1 i1_, e1_;
it2 i2_, e2_;
T fill_;
};
auto begin() {
return iterator(std::begin(c1_), std::end(c1_), std::begin(c2_), std::end(c2_), fill_);
}
auto end() {
// end는 양쪽 이터레이터를 끝으로 맞춘 상태
return iterator(std::end(c1_), std::end(c1_), std::end(c2_), std::end(c2_), fill_);
}
private:
C1& c1_;
C2& c2_;
T fill_;
};
template<typename C1, typename C2, typename T>
auto zip_longest_func(C1& c1, C2& c2, T fillvalue) {
return zip_longest_view<C1,C2,T>(c1, c2, fillvalue);
}
사용 예제:
std::vector<int> v1 = {1,2,3};
std::vector<std::string> v2 = {"a","b"};
for (auto [x,y] : zip_longest_func(v1,v2,std::string("X"))) {
std::cout << "(" << x << "," << y << ")\n";
}
// (1,a)
// (2,b)
// (3,X)
3. pairwise 구현 (기본 형태)
pairwise(iterable)는 (x1,x2), (x2,x3), (x3,x4), ... 형태로 연속된 원소들 쌍을 반환합니다.
template<typename Container>
class pairwise_view {
public:
pairwise_view(Container& c) : c_(c) {}
class iterator {
public:
using base_it = decltype(std::begin(std::declval<Container&>()));
iterator(base_it current, base_it end)
: current_(current), end_(end) {
// pairwise를 위해 current와 current+1 둘 다 접근 가능해야 함.
}
iterator& operator++() {
++current_;
return *this;
}
bool operator!=(const iterator& other) const { return current_ != other.current_; }
auto operator*() const {
auto next_it = current_;
++next_it;
return std::pair<decltype(*current_), decltype(*current_)>(*current_, *next_it);
}
private:
base_it current_;
base_it end_;
};
auto begin() {
auto b = std::begin(c_);
auto e = std::end(c_);
if (b == e) return end();
auto next = b; ++next;
if (next == e) return end(); // 원소가 1개 이하인 경우 pairwise 없음
return iterator(b,e);
}
auto end() {
return iterator(std::end(c_),std::end(c_));
}
private:
Container& c_;
};
template<typename Container>
auto pairwise_func(Container& c) {
return pairwise_view<Container>(c);
}
사용 예제:
std::vector<int> arr = {10,20,30,40};
for (auto p : pairwise_func(arr)) {
std::cout << "(" << p.first << "," << p.second << ")\n";
}
// (10,20)
// (20,30)
// (30,40)
정교한(또는 확장된) 구현: C++20 Ranges와 Concepts 활용
- count: Concepts를 활용해 정수형 타입인지, 혹은 산술연산 가능한지 제약할 수 있고, ranges 파이프라인에서 count_func()를 바로 take(10) 같은 뷰와 결합해 무한 시퀀스를 유한하게 만들 수 있습니다.
- zip_longest: Variadic template으로 확장해 여러 컨테이너를 받을 수도 있고, Concepts를 통해 입력이 Range인지 확인할 수 있습니다.
- pairwise: Ranges의 std::views::slide(2)와 유사한 개념을 활용하면 더 깔끔하게 구현할 수 있습니다(비공식 기능이지만, range-v3 같은 곳에서 지원).
성능과 메모리 측면
- count: 추가 메모리 없이 무한 시퀀스 생성.
- zip_longest: fillvalue 할당 외 별도 메모리 없음. 단, 가장 긴 컨테이너를 기준으로 진행.
- pairwise: 간단한 iterator 증가, 메모리 오버헤드 없음.
모두 lazy evaluation을 기반으로 하므로, 큰 비용 없이 필요한 원소를 순차적으로 제공할 수 있습니다.
관련 링크 및 리소스
- C++ 표준 문서:
- range-v3:
- range-v3 GitHub
slide나 view::chunk 등을 사용하면 pairwise 유사 기능을 쉽게 구현할 수 있으며, zip_longest 유사 기능도 라이브러리나 커뮤니티 구현 참고 가능.
- range-v3 GitHub
- 파이썬 itertools 문서:
마무리
이번 글에서는 파이썬의 count, zip_longest, pairwise를 C++20/23로 흉내 내봤습니다. 무한 시퀀스 생성, 길이 불일치 시 채우기, 연속된 쌍 생산 등, 파이썬 itertools가 제공하는 편리한 패턴들을 C++에서도 어느 정도 재현할 수 있음을 확인할 수 있었죠. 물론 파이썬만큼 간결하지 않거나 모든 기능을 완벽히 옮기긴 어렵지만, Ranges, Concepts 등의 현대 C++ 기술을 잘 활용하면 계속 파이썬스러운 코드를 구현하는 길은 열려 있습니다.
이 시리즈를 통해, 파이썬 itertools가 얼마나 생산적이고 강력한지, 그리고 C++20/23에서 그런 패턴을 얼마나 가까이 재현할 수 있는지 감각을 잡으셨길 바랍니다. 더 나아가, range-v3나 커뮤니티 라이브러리, 그리고 미래의 C++ 표준 확장을 통해 점점 더 파이썬스럽고 우아한 C++ 코드를 작성할 수 있을 거라 믿습니다.