앞선 글들에서 우리는 파이썬의 range, enumerate, zip, map, filter와 같은 편리한 함수형 API를 C++20/23 문법과 Ranges 라이브러리 등을 통해 “파이썬스럽게” 구현하는 방법을 살펴보았습니다. 이번 글에서는 한 단계 더 나아가, 파이썬의 itertools 라이브러리가 제공하는 몇 가지 대표적인 툴을 C++ 스타일로 흉내 내보려 합니다.
itertools는 파이썬에서 반복 가능한(iterable) 시퀀스를 다루는 데 매우 유용한 툴셋을 제공합니다. 그중에서도 chain, takewhile, dropwhile는 다양한 컨테이너나 이터러블을 직관적이고 유연하게 다룰 수 있게 해줍니다.
글의 구성은 다음과 같습니다:
- 일반적인 C++ 구현 (Before): 파이썬 chain, takewhile, dropwhile 없이 비슷한 로직을 구현하려면?
- 단순한 Python 같은 C++ 구현 (After: 첫 단추): 간단한 view를 만들어 itertools 스타일의 API를 흉내 내는 기본적인 방법.
- 정교한(또는 확장된) 구현: 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 표준 뷰는 컴파일러 최적화를 적극 유도할 수 있어, 성능 측면에서도 유리한 경우가 많습니다.
관련 링크 및 리소스
- C++ 표준 문서 (draft):
- C++ Reference & Tutorials:
- 외부 라이브러리:
- range-v3: C++20 Ranges의 토대가 된 라이브러리로, chain, take_while, drop_while 등을 포함한 풍부한 뷰 제공.
- Boost.Range: 다양한 iterator 어댑터와 range adapter 지원.
- 관련 아티클 및 블로그:
- Modernes C++ blog - Ranges and Views
- Fedor Pikus의 현대 C++ 발표들 (YouTube) - Ranges 관련 심도 있는 토크 포함.
마무리
이번 글에서는 파이썬 itertools의 핵심 기능 중 일부인 chain, takewhile, dropwhile를 C++에서 구현하는 방법을 살펴봤습니다. 단순한 직접 구현에서 출발해 Ranges, Concepts, 그리고 C++23 기능까지 활용하면서 점점 더 “파이썬스럽고” 정교한 API를 만들 수 있다는 점을 확인했습니다. C++20 이후의 기능을 잘 활용하면, 파이썬 수준의 선언적이고 직관적인 반복처리 로직을 C++에서도 구현할 수 있게 됩니다.
'개발 이야기 > C++' 카테고리의 다른 글
C++20과 C++23을 활용한 “파이썬스러운” API 구현 #6: product, permutations, combinations (0) | 2024.12.12 |
---|---|
C++20과 C++23을 활용한 “파이썬스러운” API 구현 #5: accumulate, groupby (0) | 2024.12.12 |
C++20과 C++23을 활용한 “파이썬스러운” API 구현 #3: map과 filter (0) | 2024.12.12 |
C++20과 C++23을 활용한 “파이썬스러운” API 구현 #2: zip (0) | 2024.12.10 |
C++20과 C++23을 활용한 “파이썬스러운” API 구현 #1: range와 enumerate (1) | 2024.12.10 |