이제까지 파이썬 itertools와 more-itertools의 다양한 함수를 C++로 옮기며, 현대 C++이 제공하는 Ranges, Concepts, lazy evaluation을 통해 얼마나 파이썬스러운 코드를 구현할 수 있는지 확인했습니다. 이번에는 takewhile, dropwhile, 그리고 accumulate를 C++에서 흉내 내는 방법을 살펴보겠습니다.
먼저 파이썬에서 이 함수들이 어떻게 동작하는지 간단히 살펴볼까요?
from itertools import takewhile, dropwhile, accumulate
# takewhile(predicate, iterable)
# predicate가 참인 동안만 원소를 반환하다가, 거짓이 되는 순간 멈춤
data = [1,2,3,4,5,1,2]
for x in takewhile(lambda x: x<4, data):
print(x, end=' ')
# 출력: 1 2 3
print()
# dropwhile(predicate, iterable)
# predicate가 참인 동안 원소를 버리다가, 거짓이 되는 시점부터 모든 원소를 반환
data2 = [0,0,1,2,3,0,1]
for x in dropwhile(lambda x: x==0, data2):
print(x, end=' ')
# 출력: 1 2 3 0 1
print()
# accumulate(iterable, func=operator.add)
# 누적합 또는 누적 연산을 수행
# 기본은 덧셈, 다른 연산도 가능
from operator import mul
for x in accumulate([1,2,3,4], mul):
print(x, end=' ')
# 출력: 1 2*1=2 2*3=6 6*4=24 → 1 2 6 24
print()
이제 이를 C++20/23 스타일로 옮겨보겠습니다.
1. takewhile 구현 (기본 형태)
takewhile(predicate, iterable)는 predicate가 참일 동안 원소를 반환하다가, 거짓이 되는 순간 중단합니다. 이는 이터레이터를 순회하면서 조건을 만족하지 않는 원소를 만나면 끝나는 뷰를 만들면 됩니다.
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 current, base_it end, Pred p)
: current_(current), end_(end), p_(p), finished_(false) {
check_finished();
}
iterator& operator++() {
if (current_ != end_) ++current_;
check_finished();
return *this;
}
bool operator!=(const iterator& other) const {
return finished_ != other.finished_ || current_ != other.current_;
}
auto operator*() const {
return *current_;
}
private:
void check_finished() {
if (current_ == end_ || !p_(*current_)) {
finished_ = true;
} else {
finished_ = false;
}
}
base_it current_, end_;
Pred p_;
bool finished_;
};
auto begin() {
return iterator(std::begin(c_), std::end(c_), p_);
}
auto end() {
// 끝 상태는 finished_=true인 상태
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);
}
사용 예제:
std::vector<int> data = {1,2,3,4,5,1,2};
for (auto x : takewhile_func(data, [](int x){ return x<4; })) {
std::cout << x << " "; // 1 2 3
}
std::cout << "\n";
2. dropwhile 구현 (기본 형태)
dropwhile(predicate, iterable)는 predicate가 참인 동안 원소를 버리다가, 첫 번째로 거짓이 되는 원소를 만나면 그 시점부터 모든 원소를 반환합니다.
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 current, base_it end, Pred p)
: current_(current), end_(end), p_(p), dropping_(true) {
advance_past_drop();
}
iterator& operator++() {
if (current_ != end_) ++current_;
return *this;
}
bool operator!=(const iterator& other) const {
return current_ != other.current_;
}
auto operator*() const {
return *current_;
}
private:
void advance_past_drop() {
while (current_ != end_ && dropping_) {
if (p_(*current_)) {
++current_;
} else {
dropping_ = false;
}
}
}
base_it current_, 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> data2 = {0,0,1,2,3,0,1};
for (auto x : dropwhile_func(data2, [](int x){ return x==0; })) {
std::cout << x << " "; // 1 2 3 0 1
}
std::cout << "\n";
3. accumulate 구현 (기본 형태)
accumulate(iterable, func=std::plus<>)는 누적 연산을 수행합니다. 파이썬 itertools.accumulate는 기본이 덧셈, 여기서는 템플릿으로 연산자(또는 함수객체)를 받도록 해보겠습니다.
#include <functional>
template<typename Container, typename BinaryOp = std::plus<>>
class accumulate_view {
public:
using value_type = typename Container::value_type;
accumulate_view(Container& c, BinaryOp op = {})
: c_(c), op_(op) {}
class iterator {
public:
using base_it = decltype(std::begin(std::declval<Container&>()));
iterator(base_it current, base_it end, BinaryOp op, bool start)
: current_(current), end_(end), op_(op), started_(start) {
if (current_ != end_ && started_) {
acc_ = *current_;
}
}
iterator& operator++() {
if (current_ != end_) {
++current_;
if (current_ != end_) {
acc_ = op_(acc_, *current_);
}
}
return *this;
}
bool operator!=(const iterator& other) const {
return current_ != other.current_;
}
value_type operator*() const {
return acc_;
}
private:
base_it current_, end_;
BinaryOp op_;
value_type acc_;
bool started_;
};
auto begin() {
auto b = std::begin(c_);
auto e = std::end(c_);
if (b == e) {
// 빈 컨테이너면 바로 end 반환
return iterator(e,e,op_,false);
}
return iterator(b,e,op_,true);
}
auto end() {
return iterator(std::end(c_), std::end(c_), op_, false);
}
private:
Container& c_;
BinaryOp op_;
};
template<typename Container, typename BinaryOp = std::plus<>>
auto accumulate_func(Container& c, BinaryOp op = {}) {
return accumulate_view<Container, BinaryOp>(c, op);
}
사용 예제:
#include <functional>
std::vector<int> arr = {1,2,3,4};
for (auto x : accumulate_func(arr, std::multiplies<int>())) {
std::cout << x << " "; // 1 2 6 24
}
std::cout << "\n";
정교한(또는 확장된) 구현: C++20 Ranges와 Concepts 활용
- takewhile, dropwhile: 사실 C++23에는 std::views::take_while, std::views::drop_while가 도입될 예정입니다. 이를 통해 표준 라이브러리만으로 파이썬스럽게 작성할 수 있게 될 것입니다.
- accumulate: C++20 Ranges에는 std::ranges::foldl, std::ranges::foldr 같은 기능이 정식화되진 않았지만, range-v3나 기타 라이브러리를 이용하면 유사한 누적 연산을 할 수 있습니다. Concepts를 통해 BinaryOp가 호출 가능한지, value_type 연산 가능한지 체크할 수도 있습니다.
성능과 메모리 측면
- takewhile, dropwhile: predicate 연산 외에 추가 메모리 없이 lazy evaluation.
- accumulate: 누적값을 하나 들고 반복, 필요 최소한의 메모리만 사용.
관련 링크 및 리소스
- C++ 표준 문서:
- C++23 Ranges (cppreference)
C++23에 take_while_view, drop_while_view 표준화.
- C++23 Ranges (cppreference)
- itertools 문서:
마무리
이번 글에서는 takewhile, dropwhile, 그리고 accumulate를 C++20/23 스타일로 구현해보며, 파이썬 itertools의 핵심적인 패턴들을 또 한 번 C++에 녹여냈습니다. 이제까지 다룬 예제들을 통해, 현대 C++이 제공하는 표현력과 lazy evaluation 기법, 그리고 Ranges를 적절히 활용하면 파이썬스러운 고수준 API도 결코 꿈이 아니라는 점을 재확인할 수 있었을 것입니다.
앞으로도 이런 아이디어를 바탕으로, 상황에 맞는 맞춤형 뷰와 어댑터를 만들어보면서 C++ 코드를 더욱 짧고 우아하게 만들어보시길 바랍니다.
'개발 이야기 > C++' 카테고리의 다른 글
[C++23 새기능 소개] std::byteswap (0) | 2024.12.12 |
---|---|
C++20과 C++23을 활용한 “파이썬스러운” API 구현 #14: groupby, permutations, product (0) | 2024.12.12 |
C++20과 C++23을 활용한 “파이썬스러운” API 구현 #12: unique_everseen, unique_justseen, powerset (0) | 2024.12.12 |
C++20과 C++23을 활용한 “파이썬스러운” API 구현 #11: grouper, intersperse, flatten (0) | 2024.12.12 |
C++20과 C++23을 활용한 “파이썬스러운” API 구현 #10: chain.from_iterable, batched (0) | 2024.12.12 |