C++20과 C++23을 활용한 “파이썬스러운” API 구현 #12: unique_everseen, unique_justseen, powerset

여러분, 우리는 지금까지 파이썬 itertools와 more-itertools의 다양한 기능을 C++20/23 문법으로 흉내 내는 방법을 살펴보며, 파이썬스러운 추상화를 C++에서 어떻게 구현할 수 있는지 탐구해왔습니다. 이번 글에서는 unique_everseen, unique_justseen, 그리고 powerset를 C++로 옮겨보며, 더욱 풍부한 반복 패턴을 구현하는 방법을 알아보겠습니다.

먼저 파이썬에서 이들 API를 간단히 살펴볼게요.

from more_itertools import unique_everseen, unique_justseen
from itertools import chain, combinations

# unique_everseen(iterable)
# 지금까지 본 적 없는 원소만 내보냄 (한 번 본 원소는 무시)
data = [1,2,1,3,2,4]
for x in unique_everseen(data):
    print(x, end=' ')
# 출력: 1 2 3 4

print()

# unique_justseen(iterable)
# 연속 중복 원소를 하나로 압축
data2 = [1,1,2,2,2,3,3,4,4,4]
for x in unique_justseen(data2):
    print(x, end=' ')
# 출력: 1 2 3 4

print()

# powerset(iterable)
# 모든 부분집합(멱집합)을 반환
def powerset(iterable):
    s = list(iterable)
    return chain.from_iterable(combinations(s, r) for r in range(len(s)+1))

for subset in powerset([1,2,3]):
    print(subset)
# ()
# (1,)
# (2,)
# (3,)
# (1,2)
# (1,3)
# (2,3)
# (1,2,3)

이제 이를 C++에서 구현해봅시다.

1. unique_everseen 구현 (기본 형태)

unique_everseen(iterable)는 새로운 원소를 발견할 때만 출력하고, 이미 본 원소는 스킵합니다. 이를 위해 지금까지 본 원소를 저장하는 집합이 필요합니다.

#include <unordered_set>

template<typename Container>
class unique_everseen_view {
public:
    unique_everseen_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) {
            advance_to_valid();
        }

        iterator& operator++() {
            if (current_ != end_) ++current_;
            advance_to_valid();
            return *this;
        }

        bool operator!=(const iterator& other) const {
            return current_ != other.current_;
        }

        auto operator*() const {
            return *current_;
        }

    private:
        void advance_to_valid() {
            while (current_ != end_) {
                if (seen_.find(*current_) == seen_.end()) {
                    seen_.insert(*current_);
                    break;
                } else {
                    ++current_;
                }
            }
        }

        base_it current_, end_;
        std::unordered_set<typename Container::value_type> seen_;
    };

    auto begin() {
        return iterator(std::begin(c_), std::end(c_));
    }
    auto end() {
        return iterator(std::end(c_), std::end(c_));
    }

private:
    Container& c_;
};

template<typename Container>
auto unique_everseen_func(Container& c) {
    return unique_everseen_view<Container>(c);
}

사용 예제:

std::vector<int> data = {1,2,1,3,2,4};
for (auto x : unique_everseen_func(data)) {
    std::cout << x << " "; // 1 2 3 4
}
std::cout << "\n";

2. unique_justseen 구현 (기본 형태)

unique_justseen(iterable)는 연속된 동일 원소를 하나로 압축합니다. 바로 이전 원소와 다를 때만 출력하면 됩니다.

template<typename Container>
class unique_justseen_view {
public:
    unique_justseen_view(Container& c) : c_(c) {}

    class filter_iterator {
    public:
        using base_it = decltype(std::begin(std::declval<Container&>()));
        filter_iterator(base_it current, base_it end)
            : current_(current), end_(end), first_(true) {
            advance_to_valid();
        }

        filter_iterator& operator++() {
            if (current_ != end_) {
                prev_ = *current_;
                ++current_;
                advance_to_valid();
            }
            return *this;
        }

        bool operator!=(const filter_iterator& other) const {
            return current_ != other.current_;
        }

        auto operator*() const {
            return *current_;
        }

    private:
        void advance_to_valid() {
            while (current_ != end_) {
                auto val = *current_;
                if (first_ || val != prev_) {
                    // 새로운 값 발견
                    break;
                } else {
                    ++current_;
                }
            }
            first_ = false;
        }

        base_it current_, end_;
        typename Container::value_type prev_;
        bool first_;
    };

    auto begin() {
        return filter_iterator(std::begin(c_), std::end(c_));
    }
    auto end() {
        return filter_iterator(std::end(c_), std::end(c_));
    }

private:
    Container& c_;
};

template<typename Container>
auto unique_justseen_func(Container& c) {
    return unique_justseen_view<Container>(c);
}

사용 예제:

std::vector<int> data2 = {1,1,2,2,2,3,3,4,4,4};
for (auto x : unique_justseen_func(data2)) {
    std::cout << x << " "; // 1 2 3 4
}
std::cout << "\n";

3. powerset 구현 (기본 형태)

powerset(iterable)은 모든 부분집합을 반환하는 함수로, 비트마스크를 이용해 구현할 수 있습니다.

#include <cmath>

template<typename Container>
class powerset_view {
public:
    powerset_view(Container& c) : c_(c) {
        sz_ = std::distance(std::begin(c_), std::end(c_));
    }

    class iterator {
    public:
        using base_it = decltype(std::begin(std::declval<Container&>()));

        iterator(base_it start, std::size_t sz, std::size_t idx)
            : start_(start), sz_(sz), idx_(idx) {}

        iterator& operator++() {
            ++idx_;
            return *this;
        }

        bool operator!=(const iterator& other) const {
            return idx_ != other.idx_;
        }

        auto operator*() const {
            std::vector<typename Container::value_type> subset;
            auto it = start_;
            for (std::size_t i = 0; i < sz_; ++i) {
                if (idx_ & (1ULL << i)) {
                    subset.push_back(*it);
                }
                ++it;
            }
            return subset;
        }

    private:
        base_it start_;
        std::size_t sz_;
        std::size_t idx_;
    };

    auto begin() {
        return iterator(std::begin(c_), sz_, 0);
    }
    auto end() {
        return iterator(std::begin(c_), sz_, (1ULL << sz_));
    }

private:
    Container& c_;
    std::size_t sz_;
};

template<typename Container>
auto powerset_func(Container& c) {
    return powerset_view<Container>(c);
}

사용 예제:

std::vector<int> arr = {1,2,3};
for (auto subset : powerset_func(arr)) {
    std::cout << "(";
    for (auto x : subset) std::cout << x << ",";
    std::cout << ")\n";
}

정교한(또는 확장된) 구현: C++20 Ranges와 Concepts 활용

  • unique_everseen: Concepts를 활용해 hash 가능하거나 equality 비교 가능한 타입을 제약.
  • unique_justseen: 연속 중복 제거는 Ranges 파이프 등과 결합해 파이프라인 형식으로 쉽게 조립 가능.
  • powerset: Concepts로 컨테이너 타입 제약 가능. Ranges와 결합해 data | powerset_func() 형태.

성능과 메모리 측면

  • unique_everseen: 모든 본 원소를 저장하므로 메모리 사용 증가. 하지만 한 번 본 원소는 즉시 필터링.
  • unique_justseen: O(1) 추가 메모리, 유효한 성능으로 연속 중복 제거.
  • powerset: 2^n 부분집합 생성. 대규모 데이터에선 매우 비싸지만, lazy하게 필요할 때마다 vector 할당.

관련 링크 및 리소스

마무리

이번 글에서는 unique_everseen, unique_justseen, 그리고 powerset를 C++20/23 스타일로 구현해보았습니다. 점점 더 다양한 파이썬 함수들을 C++로 옮기는 과정을 통해, 현대 C++이 제공하는 lazy evaluation과 고차 함수형 패턴의 가능성을 재확인했습니다. 이제 여러분도 이런 패턴을 활용해 더욱 우아하고 “파이썬스러운” C++ 코드를 작성해보시길 바랍니다.

반응형