안녕하세요! 지난 글들에서 우리는 파이썬 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 길이에 비례하는 정도이며, 모든 조합을 한 번에 생성하지 않으므로 무리한 메모리 사용은 없습니다.
관련 링크 및 리소스
- C++ 표준 문서:
- range-v3:
- range-v3 GitHub
- 해당 라이브러리나 추가 라이브러리를 참고하면 더 많은 itertools 스타일 기능을 찾거나 구현에 참고할 수 있습니다.
- 파이썬 itertools 문서:
마무리
이번 글에서는 compress, filterfalse, starmap, combinations_with_replacement를 C++20/23 스타일로 흉내 내봤습니다. 각 기능을 통해 파이썬 itertools의 풍부한 표현력을 C++에 어떻게 녹일 수 있는지 확인할 수 있었습니다. C++20 Ranges, Concepts, 그리고 코루틴을 비롯한 현대적 기능들이 적극적으로 활용되면, 앞으로 더 “파이썬스럽고” 간결한 C++ 코드를 짤 수 있는 기반이 더욱 탄탄해질 것입니다.
이제까지 여러 itertools 기능을 다뤄왔으니, 여러분도 프로젝트나 실험 코드를 통해 이런 기법들을 활용해보시면 어떨까요?