여러분, 지난 글들에서 우리는 파이썬 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 이터레이션.
관련 링크 및 리소스
- C++ 표준 문서:
- range-v3:
- range-v3 GitHub
- views::intersperse, views::join 등의 뷰를 통해 intersperse, flatten 유사 기능 지원.
- more-itertools 문서:
마무리
이번 글에서 다룬 grouper, intersperse, flatten는 파이썬의 강력한 이터레이션 패턴을 C++로 옮기는 또 다른 예시입니다. 이렇게 직접 구현해보면 파이썬이 얼마나 간결한 문법을 갖추고 있는지, 그리고 C++20/23 이후로 얼마나 표현력이 강화되었는지 새삼 느끼게 됩니다.
여러분도 이 아이디어를 확장해, 프로젝트에서 더욱 우아한 C++ 코드를 작성해보세요. 현대 C++과 파이썬 itertools 패턴의 결합은 생각보다 많은 가능성을 열어줍니다.