C++ Generator 완벽 가이드 | co_yield로 lazy 시퀀스·무한 수열·파이프라인 만들기

C++ Generator 완벽 가이드 | co_yield로 lazy 시퀀스·무한 수열·파이프라인 만들기

이 글의 핵심

메모리 부족·OOM 해결, Generator 완전 구현(co_yield·promise_type), lazy 평가·무한 시퀀스, filter·map·take 조합, 자주 발생하는 에러(dangling·lifetime)·해결법, 성능 비교, 파일 읽기·데이터 스트리밍 프로덕션 패턴.

들어가며: “시퀀스를 만들다 메모리 부족 에러가 나요”

실제 겪는 문제 시나리오

// ❌ 문제: 대용량 시퀀스가 필요한데...
// - 10GB 로그 파일에서 ERROR 라인만 앞 100개 찾기
// - API 페이지네이션: 100만 건 중 처음 20개만 표시
// - 피보나치·소수 같은 무한 수열 — 벡터로는 표현 불가

// vector에 전부 담으면 → OOM(Out of Memory)
// 전체를 미리 계산하면 → 불필요한 CPU·메모리 낭비

시나리오 1 (로그 분석): 10GB 로그에서 ERROR 라인만 앞 100개 추출. vector<string>에 전체를 올리면 RAM 부족으로 크래시. 필요한 만큼만 스트리밍으로 읽어야 합니다.

시나리오 2 (API 페이지네이션): DB에 100만 건. 처음 20개만 화면에 표시하는데, 전부 fetch하면 네트워크·메모리 낭비. Generator로 “요청할 때마다 다음 N개”만 가져오는 패턴이 적합합니다.

시나리오 3 (무한 수열): 피보나치, 소수, 카운터처럼 끝이 없는 시퀀스. 벡터는 크기를 미리 알아야 하므로 표현 불가. Generator는 co_yield로 “다음 값 하나”만 내보내므로 무한 시퀀스를 자연스럽게 표현합니다.

시나리오 4 (데이터 파이프라인): filter → map → take처럼 여러 단계를 거치는 처리. 중간 결과를 전부 벡터에 담으면 메모리 폭발. Generator 파이프라인은 값이 흐르는 대로 한 개씩 처리합니다.

실제 프로덕션에서 겪는 문제들:

  • OOM: 대용량 시퀀스를 vector에 담다 메모리 부족
  • 초기 지연: 전체 생성이 끝날 때까지 블로킹
  • 불필요한 계산: 앞 10개만 쓰는데 100만 개 전부 계산
  • 무한 시퀀스 표현 불가: 벡터는 크기 한계가 있음

해결책: Generatorco_yield필요할 때마다 값 하나씩 만들어 주는 lazy 시퀀스. 100만 개를 미리 만들지 않고, next() 호출 시마다 다음 값만 계산합니다.

문제 상황 (요약)

0부터 100만까지 정수를 만들어야 하는데, 앞 10개만 쓰고 끝내는 경우가 있습니다. 전통적인 방식은 std::vector<int>에 100만 개를 전부 채워 넣는 것입니다. 그러면 약 4MB 메모리가 한 번에 할당되고, 99만 9,990개는 결국 버려집니다. 시퀀스가 더 크거나 요소 타입이 더 무거우면 메모리 부족이나 지연이 심해집니다.

제너레이터(Generator)필요할 때마다 값 하나씩 만들어 주는 lazy 시퀀스입니다. 100만 개를 미리 만들지 않고, next()를 호출할 때마다 다음 값만 계산합니다. 10개만 쓰고 멈추면 나머지는 계산하지 않으므로 메모리와 CPU를 절약할 수 있습니다.

목표:

  • 문제 시나리오: 메모리를 많이 쓰는 시퀀스 생성
  • Generator 완전 구현: promise_type, iterator, 범위 기반 for
  • 무한 제너레이터: 끝이 없는 시퀀스
  • 제너레이터 조합: filter, map, 파이프라인
  • 자주 발생하는 오류: lifetime(수명) 이슈
  • 성능 비교: generator vs vector
  • 프로덕션 패턴: 파일 읽기, 데이터 스트리밍

이 글을 읽으면:

  • co_yield로 lazy 시퀀스를 만드는 방법을 알 수 있습니다.
  • Generator 타입을 처음부터 구현할 수 있습니다.
  • 무한 시퀀스와 조합 패턴을 활용할 수 있습니다.
  • lifetime 관련 실수를 피할 수 있습니다.
  • 실무에서 파일·스트리밍 처리에 적용할 수 있습니다.

요구 환경: C++20 (g++ -std=c++20 또는 clang++ -std=c++20). C++23에서는 std::generator를 사용할 수 있습니다.


목차

  1. 문제 시나리오: 메모리를 많이 쓰는 시퀀스 생성
  2. Lazy 평가 (지연 평가) 이해하기
  3. Generator 완전 구현
  4. 무한 제너레이터
  5. 제너레이터 조합
  6. 자주 발생하는 오류 (lifetime 수명)
  7. 모범 사례 (Best Practices)
  8. 성능 비교: generator vs vector
  9. 프로덕션 패턴: 파일 읽기, 데이터 스트리밍
  10. 정리

1. 문제 시나리오: 메모리를 많이 쓰는 시퀀스 생성

벡터 방식의 한계

대용량 시퀀스를 만들 때 std::vector를 쓰면 전체를 한 번에 메모리에 올립니다.

// ❌ 100만 개 전부 할당 — 앞 10개만 쓰는데 4MB 사용
std::vector<int> createSequence(int n) {
    std::vector<int> vec;
    vec.reserve(n);
    for (int i = 0; i < n; ++i) {
        vec.push_back(i);
    }
    return vec;
}

int main() {
    auto seq = createSequence(1'000'000);
    for (int i = 0; i < 10; ++i) {  // 10개만 사용
        process(seq[i]);
    }
    // 99만 9,990개는 메모리에 그대로 — 낭비
}

문제점:

  • 메모리: n이 크면 O(n) 메모리 사용
  • 초기 지연: 전체 시퀀스 생성이 끝날 때까지 대기
  • 불필요한 계산: 앞부분만 쓰면 나머지 계산이 낭비

Generator로 해결

Generator는 호출할 때마다 다음 값 하나만 만들어 줍니다. 필요한 만큼만 계산하고, 나머지는 생성하지 않습니다.

// ✅ Generator: 필요한 만큼만 계산
Generator<int> count(int n) {
    for (int i = 0; i < n; ++i) {
        co_yield i;  // i를 넘기고 일시 정지
    }
}

int main() {
    auto gen = count(1'000'000);
    for (int i = 0; i < 10; ++i) {
        auto opt = gen.next();  // 10번만 계산
        if (opt) process(*opt);
    }
    // 나머지 99만 9,990개는 계산하지 않음
}

장점:

  • 메모리: O(1) — 현재 값 하나만 유지
  • 지연: 첫 값은 즉시, 이후 값은 요청 시마다
  • lazy: 필요한 만큼만 계산

시각화

flowchart TB
    subgraph vector["벡터 방식"]
        V1[createSequence 호출] --> V2[100만 개 전부 할당]
        V2 --> V3[메모리 ~4MB]
        V3 --> V4[10개만 사용]
        V4 --> V5[99만 9,990개 낭비]
    end
    subgraph generator["Generator 방식"]
        G1[count 호출] --> G2[첫 co_yield까지 대기]
        G2 --> G3[next 10번 호출]
        G3 --> G4[10개만 계산]
        G4 --> G5[나머지 미계산]
    end

추가 활용 사례

  • 트리/그래프 DFS: 방문한 노드만 co_yield — 전체 경로 저장 불필요
  • 조건부 조기 종료: primes()에서 p > n인 첫 소수 찾을 때까지만 계산
  • 임베디드: 1MB RAM 장치에서 vector 할당 실패 시 Generator로 스트리밍
  • 실시간 스트리밍: 센서·이벤트 로그처럼 끝이 없는 스트림 (비동기는 co_await와 결합)

2. Lazy 평가 (지연 평가) 이해하기

Lazy vs Eager

Eager(즉시 평가): 시퀀스를 한 번에 전부 계산합니다. vectorpush_back으로 채우는 방식이 여기에 해당합니다.

Lazy(지연 평가): 값이 필요할 때만 계산합니다. co_yield는 “이 값을 넘기고 일시 정지”하므로, 호출자가 다음 값을 요청할 때까지 다음 계산이 미뤄집니다.

// Eager: 100만 개 전부 계산 후 반환
std::vector<int> eager = createSequence(1'000'000);  // 즉시 100만 개 생성

// Lazy: 10개만 요청하면 10개만 계산
auto gen = count(1'000'000);
for (int i = 0; i < 10; ++i) {
    auto v = gen.next();  // i번째 요청 시에만 i번째 값 계산
}

Lazy 평가 흐름

sequenceDiagram
    participant C as 호출자
    participant G as Generator

    C->>G: next() 1회
    G->>G: co_yield 0
    G-->>C: 0 반환, 일시 정지
    C->>C: 0 사용
    C->>G: next() 2회
    G->>G: co_yield 1
    G-->>C: 1 반환
    Note over G: 2, 3, 4... 는 아직 계산 안 함

핵심: count(1'000'000)을 호출해도 100만 개를 만들지 않습니다. next()를 10번 호출하면 10개만 계산하고, 나머지는 생성되지 않습니다.

Lazy의 장점

항목Eager (vector)Lazy (Generator)
메모리O(n)O(1)
초기 지연전체 생성 완료까지첫 값만
조기 종료나머지도 이미 계산됨요청 안 하면 미계산
무한 시퀀스불가가능

3. Generator 완전 구현

promise_type 설계

Generator의 동작은 promise_type으로 제어합니다. 이전 글(#23-1)에서 다룬 코루틴 기초를 바탕으로, co_yield에 맞는 promise를 정의합니다.

#include <coroutine>
#include <exception>
#include <optional>

template <typename T>
class Generator {
public:
    struct promise_type {
        T current_value;
        std::exception_ptr exception;

        // co_yield value 호출 시: 값을 저장하고 일시 정지
        std::suspend_always yield_value(T value) {
            current_value = std::move(value);
            return {};
        }

        // 코루틴 진입 직후 일시 정지 — 첫 next()에서 resume
        std::suspend_always initial_suspend() { return {}; }

        // co_return 후 일시 정지 — 호출자가 done() 확인 가능
        std::suspend_always final_suspend() noexcept { return {}; }

        void return_void() {}
        void unhandled_exception() { exception = std::current_exception(); }

        Generator get_return_object() {
            return Generator{
                std::coroutine_handle<promise_type>::from_promise(*this)
            };
        }
    };

    // ... (아래 계속)
};

핵심 포인트:

  • yield_value(T): co_yield 값 저장 후 suspend_always로 일시 정지
  • initial_suspend() = suspend_always: 첫 resume() 전까지 대기
  • final_suspend() = suspend_always: 종료 후 done() 확인 가능

iterator와 범위 기반 for

begin()/end()를 제공하면 범위 기반 for로 사용할 수 있습니다.

    struct iterator {
        std::coroutine_handle<promise_type> handle;
        bool done;

        iterator(std::coroutine_handle<promise_type> h, bool d)
            : handle(h), done(d) {}

        T operator*() const {
            return handle.promise().current_value;
        }

        iterator& operator++() {
            handle.resume();
            if (handle.promise().exception)
                std::rethrow_exception(handle.promise().exception);
            done = handle.done();
            return *this;
        }

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

    explicit Generator(std::coroutine_handle<promise_type> h) : handle_(h) {}
    ~Generator() {
        if (handle_) handle_.destroy();
    }

    Generator(const Generator&) = delete;
    Generator& operator=(const Generator&) = delete;
    Generator(Generator&& other) noexcept : handle_(other.handle_) {
        other.handle_ = nullptr;
    }
    Generator& operator=(Generator&& other) noexcept {
        if (this != &other) {
            if (handle_) handle_.destroy();
            handle_ = other.handle_;
            other.handle_ = nullptr;
        }
        return *this;
    }

    iterator begin() {
        handle_.resume();
        if (handle_.promise().exception)
            std::rethrow_exception(handle_.promise().exception);
        return iterator{handle_, handle_.done()};
    }
    iterator end() { return iterator{handle_, true}; }

private:
    std::coroutine_handle<promise_type> handle_;
};

next() 인터페이스 (선택)

next()로 값을 하나씩 꺼내는 인터페이스도 추가할 수 있습니다.

    std::optional<T> next() {
        if (!handle_ || handle_.done()) return std::nullopt;
        handle_.resume();
        if (handle_.done()) return std::nullopt;
        return handle_.promise().current_value;
    }

완전한 예제

// g++ -std=c++20 -o generator_basic generator_basic.cpp && ./generator_basic
#include <coroutine>
#include <exception>
#include <iostream>
#include <optional>

template <typename T>
class Generator {
public:
    struct promise_type {
        T current_value;
        std::exception_ptr exception;

        std::suspend_always yield_value(T value) {
            current_value = std::move(value);
            return {};
        }
        std::suspend_always initial_suspend() { return {}; }
        std::suspend_always final_suspend() noexcept { return {}; }
        void return_void() {}
        void unhandled_exception() { exception = std::current_exception(); }

        Generator get_return_object() {
            return Generator{
                std::coroutine_handle<promise_type>::from_promise(*this)};
        }
    };

    struct iterator {
        std::coroutine_handle<promise_type> handle;
        bool done;
        iterator(std::coroutine_handle<promise_type> h, bool d)
            : handle(h), done(d) {}
        T operator*() const { return handle.promise().current_value; }
        iterator& operator++() {
            handle.resume();
            if (handle.promise().exception)
                std::rethrow_exception(handle.promise().exception);
            done = handle.done();
            return *this;
        }
        bool operator!=(const iterator& other) const {
            return done != other.done;
        }
    };

    explicit Generator(std::coroutine_handle<promise_type> h) : handle_(h) {}
    ~Generator() { if (handle_) handle_.destroy(); }
    Generator(const Generator&) = delete;
    Generator& operator=(const Generator&) = delete;
    Generator(Generator&& other) noexcept : handle_(other.handle_) {
        other.handle_ = nullptr;
    }

    iterator begin() {
        handle_.resume();
        if (handle_.promise().exception)
            std::rethrow_exception(handle_.promise().exception);
        return iterator{handle_, handle_.done()};
    }
    iterator end() { return iterator{handle_, true}; }

    std::optional<T> next() {
        if (!handle_ || handle_.done()) return std::nullopt;
        handle_.resume();
        if (handle_.done()) return std::nullopt;
        return handle_.promise().current_value;
    }

private:
    std::coroutine_handle<promise_type> handle_;
};

Generator<int> count(int n) {
    for (int i = 0; i < n; ++i) {
        co_yield i;
    }
}

int main() {
    for (int x : count(5)) {
        std::cout << x << " ";  // 0 1 2 3 4
    }
    std::cout << "\n";
}

실행 결과: 0 1 2 3 4

co_yield 흐름 다이어그램

sequenceDiagram
    participant C as 호출자
    participant G as Generator
    participant P as promise_type
    participant Co as 코루틴

    C->>G: begin()
    G->>Co: handle_.resume()
    Co->>P: yield_value(0)
    P-->>Co: suspend_always
    Co-->>G: 일시 정지
    G-->>C: iterator(0 반환)
    C->>C: *it → 0
    C->>G: ++it
    G->>Co: handle_.resume()
    Co->>P: yield_value(1)
    Note over Co: ... 반복 ...

4. 무한 제너레이터

끝이 없는 시퀀스

Generator는 끝이 없는 시퀀스도 표현할 수 있습니다. co_return이 없으면 이론적으로 무한히 co_yield할 수 있고, 호출하는 쪽에서 필요한 만큼만 꺼내 쓰면 됩니다.

// 무한 피보나치 수열
Generator<long long> fibonacci() {
    long long a = 0, b = 1;
    while (true) {
        co_yield a;
        auto next = a + b;
        a = b;
        b = next;
    }
}

int main() {
    int count = 0;
    for (long long x : fibonacci()) {
        std::cout << x << " ";
        if (++count >= 15) break;  // 15개만 출력
    }
    // 0 1 1 2 3 5 8 13 21 34 55 89 144 233 377
}

무한 카운터

Generator<int> counter(int start = 0) {
    int i = start;
    while (true) {
        co_yield i++;
    }
}

// 사용: zip 등에 활용
auto gen = counter(10);
auto it = gen.begin();
for (int i = 0; i < 5; ++i) {
    std::cout << *it << " ";
    ++it;
}
// 10 11 12 13 14

소수 생성기

Generator<int> primes() {
    co_yield 2;
    for (int n = 3; ; n += 2) {
        bool is_prime = true;
        for (int d = 3; d * d <= n; d += 2) {
            if (n % d == 0) { is_prime = false; break; }
        }
        if (is_prime) co_yield n;
    }
}

// 처음 10개 소수
int n = 0;
for (int p : primes()) {
    std::cout << p << " ";
    if (++n >= 10) break;
}
// 2 3 5 7 11 13 17 19 23 29

주의사항

무한 Generator를 범위 기반 for에 그대로 넣으면 무한 루프가 됩니다. 반드시 break나 개수 제한을 두어야 합니다.

// ❌ 위험: 무한 루프
for (auto x : fibonacci()) {
    process(x);  // 절대 끝나지 않음
}

// ✅ 안전: 개수 제한
int n = 0;
for (auto x : fibonacci()) {
    process(x);
    if (++n >= 100) break;
}

5. 제너레이터 조합

filter: 조건에 맞는 값만

다른 Generator를 받아 조건을 만족하는 값만 co_yield하는 Generator를 만들 수 있습니다.

template <typename T, typename Pred>
Generator<T> filter(Generator<T> src, Pred pred) {
    for (T x : src) {
        if (pred(x)) co_yield x;
    }
}

// 사용: 짝수만
auto nums = count(20);
auto evens = filter(std::move(nums),  { return x % 2 == 0; });
for (int x : evens) {
    std::cout << x << " ";  // 0 2 4 6 8 10 12 14 16 18
}

map: 값 변환

template <typename T, typename F>
Generator<std::invoke_result_t<F, T>> map(Generator<T> src, F f) {
    for (T x : src) {
        co_yield f(x);
    }
}

// 사용: 제곱
auto nums = count(5);
auto squares = map(std::move(nums),  { return x * x; });
for (int x : squares) {
    std::cout << x << " ";  // 0 1 4 9 16
}

파이프라인

filter와 map을 연결해 파이프라인을 구성할 수 있습니다.

// count(100) → 짝수만 → 제곱 → 10개만
auto nums = count(100);
auto evens = filter(std::move(nums),  { return x % 2 == 0; });
auto squares = map(std::move(evens),  { return x * x; });

int n = 0;
for (int x : squares) {
    std::cout << x << " ";
    if (++n >= 10) break;
}
// 0 4 16 36 64 100 144 196 256 324

take: 상위 N개만

template <typename T>
Generator<T> take(Generator<T> src, size_t n) {
    size_t count = 0;
    for (T x : src) {
        if (count++ >= n) break;
        co_yield x;
    }
}

// 사용
for (int x : take(count(1000), 5)) {
    std::cout << x << " ";  // 0 1 2 3 4
}

zip: 두 시퀀스 병합

template <typename T, typename U>
Generator<std::pair<T, U>> zip(Generator<T> a, Generator<U> b) {
    auto it_a = a.begin();
    auto it_b = b.begin();
    auto end_a = a.end();
    auto end_b = b.end();
    while (it_a != end_a && it_b != end_b) {
        co_yield {*it_a, *it_b};
        ++it_a;
        ++it_b;
    }
}

// 사용
auto nums = count(5);
auto letters = /* Generator<char> 예시 */;
for (auto [n, c] : zip(std::move(nums), std::move(letters))) {
    std::cout << "(" << n << "," << c << ") ";
}

enumerate, sliding_window, flatten, distinct

  • enumerate: {idx++, x}로 인덱스와 값 쌍 반환 (Python enumerate()와 동일)
  • sliding_window: 연속 N개 구간 — 이동 평균, 패턴 탐지에 활용
  • flatten: Generator<Generator<T>>Generator<T> 평탄화
  • distinct: std::unordered_set으로 중복 제거
template <typename T>
Generator<std::pair<size_t, T>> enumerate(Generator<T> src) {
    size_t idx = 0;
    for (T x : src) co_yield {idx++, x};
}

template <typename T>
Generator<T> distinct(Generator<T> src) {
    std::unordered_set<T> seen;
    for (T x : src)
        if (seen.insert(x).second) co_yield x;
}

6. 자주 발생하는 오류 (lifetime 수명)

1. Dangling 참조 (죽은 참조)

원인: co_yield참조를 반환하고, 그 참조가 가리키는 객체가 코루틴 재개 전에 소멸하는 경우.

// ❌ 위험: string_view는 참조 — 임시 string 소멸 후 접근
Generator<std::string_view> splitBad(std::string_view s) {
    size_t pos = 0;
    while (pos < s.size()) {
        auto end = s.find(' ', pos);
        if (end == std::string_view::npos) end = s.size();
        co_yield s.substr(pos, end - pos);  // UB 가능
        pos = end == s.size() ? end : end + 1;
    }
}

// 호출 시
for (auto part : splitBad(std::string("a b c"))) {  // 임시 string
    // part가 이미 소멸한 임시를 참조 — UB
}

// ✅ 안전: 값으로 복사
Generator<std::string> splitGood(std::string s) {
    size_t pos = 0;
    while (pos < s.size()) {
        auto end = s.find(' ', pos);
        if (end == std::string_view::npos) end = s.size();
        co_yield s.substr(pos, end - pos);
        pos = end == s.size() ? end : end + 1;
    }
}

2. Generator 이동 후 사용

원인: Generator는 이동 전용입니다. filtermap에 넘긴 뒤 원본을 다시 쓰면 안 됩니다.

auto nums = count(10);
auto evens = filter(std::move(nums),  { return x % 2 == 0; });
// nums는 이미 비어 있음 (handle이 이동됨)

for (int x : nums) { }   // ❌ UB: 빈 Generator
for (int x : evens) { }  // ✅ OK

3. 핸들 수명 관리

원인: coroutine_handle만 복사해 두고, Generator 객체는 소멸시킨 뒤 핸들로 resume 호출.

// ❌ 위험
std::coroutine_handle<> dangerous;
{
    auto gen = count(5);
    dangerous = gen.handle();  // 핸들만 복사 (실제로는 handle_이 private)
}  // gen 소멸 → destroy() 호출
dangerous.resume();  // UB: 이미 해제된 프레임

// ✅ 안전: Generator 객체가 살아 있는 동안만 사용
auto gen = count(5);
for (int x : gen) { /* ... */ }

4. iterator 무효화

원인: begin()으로 얻은 iterator를 사용하는 동안 Generator를 이동하거나 소멸하면 iterator가 무효화됩니다.

auto gen = count(5);
auto it = gen.begin();
gen = count(10);  // gen 이동 — it가 가리키는 handle 무효
++it;  // ❌ UB

5. 지역 변수 참조 co_yield

원인: 스택에 있는 지역 변수의 주소나 참조를 co_yield하면, 코루틴이 일시 정지된 동안 그 스택은 유지되지만, 호출자에게 반환된 값이 참조 타입이면 호출자가 사용할 때 이미 소멸했을 수 있습니다.

// ❌ 위험
Generator<const std::string&> bad() {
    std::string local = "hello";
    co_yield local;  // local의 참조 반환
}  // 코루틴 일시 정지 — local은 프레임에 있음
// 하지만 호출자가 *it를 쓰는 시점에 "const string&"이
// 프레임 내 local을 가리키므로, 이 경우는 유효할 수 있음.
// 문제: Generator가 이동되거나 소멸되면 dangling

// ✅ 안전: 값으로 반환
Generator<std::string> good() {
    std::string local = "hello";
    co_yield local;  // 복사본 반환
}

6. 반복 begin() 호출

원인: begin()을 여러 번 호출하면 코루틴이 여러 번 resume되어 순회가 꼬입니다. 범위 기반 for는 begin()을 한 번만 호출하므로 안전합니다.

// ❌ 위험: begin() 두 번 호출
auto gen = count(5);
auto it1 = gen.begin();
auto it2 = gen.begin();  // it1과 handle 공유 → 순회 꼬임

// ✅ 안전
for (int x : count(5)) { /* ... */ }

7. 재귀 Generator와 스택 오버플로우

원인: dfs(child)처럼 재귀 호출 시 깊은 트리에서 스택 오버플로우. 코루틴 프레임은 힙에 있지만 for 루프는 호출 스택을 사용합니다. 깊이 제한을 두거나 반복+스택으로 전환하세요.

8. take() off-by-one 오류

원인: count > ncount >= n 혼동 시 yield 개수 오류.

// ❌ count > n → n=5일 때 6개 yield
if (count > n) break;

// ✅ count >= n → 정확히 n개
if (count >= n) break;

7. 모범 사례 (Best Practices)

항목권장비권장
타입값 타입 Generator<std::string>참조 Generator<std::string_view> (lifetime 위험)
무한 시퀀스take() 또는 루프 내 break상한 없이 순회
이동filter(std::move(nums), pred) 후 nums 미사용이동 후 원본 재사용
예외호출 측 try/catch예외 무시
성능파이프라인 단계 최소화, co_yield std::move(x)filter→map→take 과다 연결

8. 성능 비교: generator vs vector

시나리오: 0 ~ 1,000,000 정수 시퀀스

방식메모리 (대략)초기 지연10개만 사용 시
vector<int>~4MB전체 할당 완료까지4MB 유지, 99만 9,990개 낭비
Generator<int>~수백 바이트 (프레임)거의 없음10개만 계산

벤치마크 예시

#include <chrono>
#include <numeric>
#include <vector>

// 벡터: 100만 개 전부 할당
void bench_vector() {
    auto start = std::chrono::high_resolution_clock::now();
    std::vector<int> vec(1'000'000);
    std::iota(vec.begin(), vec.end(), 0);
    int sum = 0;
    for (int i = 0; i < 10; ++i) sum += vec[i];
    auto end = std::chrono::high_resolution_clock::now();
    // 할당 + 10개 접근
}

// Generator: 10개만 계산
void bench_generator() {
    auto start = std::chrono::high_resolution_clock::now();
    auto gen = count(1'000'000);
    int sum = 0;
    int n = 0;
    for (int x : gen) {
        sum += x;
        if (++n >= 10) break;
    }
    auto end = std::chrono::high_resolution_clock::now();
    // 10번 co_yield만
}

예상 결과 (환경에 따라 다름):

  • vector: 초기 할당 수 ms ~ 수십 ms
  • Generator: 수 μs ~ 수십 μs (10개만 계산)

언제 어떤 방식을 쓸까?

상황권장
전체 순회가 확정vector (캐시 친화적, 단순)
앞 N개만 필요Generator
무한/매우 큰 시퀀스Generator
요소 타입이 무거움Generator (복사 최소화)
랜덤 접근 필요vector

메모리 사용량 비교 다이어그램

flowchart LR
    subgraph vec["vector (100만 int)"]
        V1[4MB 힙]
    end
    subgraph gen["Generator (100만)"]
        G1[코루틴 프레임 ~1KB]
    end

성능 최적화 팁

1. 작은 타입 사용

co_yield할 때마다 값이 promise의 current_value에 복사됩니다. int, double 같은 작은 타입은 복사 비용이 거의 없습니다. std::string은 이동 의미론을 활용해 co_yield std::move(str)로 넘기면 복사 대신 이동이 일어납니다.

// ✅ 이동으로 복사 비용 절감
Generator<std::string> lines() {
    std::string line;
    while (std::getline(file, line)) {
        co_yield std::move(line);  // line은 다음 반복에서 새로 채워짐
    }
}

2. 파이프라인 단계 최소화

filter → map → take처럼 여러 Generator를 연결하면, 각 값마다 여러 코루틴이 resume/suspend를 반복합니다. 단계가 많을수록 오버헤드가 커집니다. 가능하면 한 Generator에서 여러 조건을 처리하는 편이 빠릅니다.

// ❌ 3단계: filter → map → take (각 값마다 3번 suspend/resume)
auto r = take(map(filter(count(100), pred), f), 10);

// ✅ 1단계: 한 Generator에서 처리
Generator<int> combined() {
    int n = 0;
    for (int i = 0; i < 100 && n < 10; ++i) {
        if (pred(i)) { co_yield f(i); ++n; }
    }
}

3. 전체 순회가 확정이면 vector

앞 N개만 쓰는 게 아니라 전체를 여러 번 순회하거나, 랜덤 접근이 필요하면 vector가 캐시 친화적이고 단순합니다. Generator는 “필요한 만큼만”이 명확할 때 선택하세요.

4. 인라인과 LTO

Generator의 resume()/yield_value() 호출이 인라인되면 분기 비용이 줄어듭니다. -O2 이상과 LTO(Link Time Optimization)를 사용하세요.

g++ -std=c++20 -O3 -flto -o app main.cpp

5. 벤치마크 작성 시 주의

Generator는 “첫 값까지의 지연”과 “값 하나당 비용”을 따로 측정하는 것이 좋습니다. 전체 순회 시간만 보면 컴파일러 최적화에 따라 결과가 달라질 수 있습니다.

// 의미 있는 벤치마크: 10개만 꺼내는 시간
auto start = std::chrono::high_resolution_clock::now();
int sum = 0, n = 0;
for (int x : count(1'000'000)) {
    sum += x;
    if (++n >= 10) break;
}
auto end = std::chrono::high_resolution_clock::now();

9. 프로덕션 패턴: 파일 읽기, 데이터 스트리밍

파일 라인별 읽기

대용량 파일을 한 줄씩 스트리밍으로 읽을 때 Generator를 쓰면 메모리를 절약할 수 있습니다.

#include <fstream>
#include <string>

Generator<std::string> readLines(const std::string& path) {
    std::ifstream f(path);
    if (!f) {
        throw std::runtime_error("Cannot open: " + path);
    }
    std::string line;
    while (std::getline(f, line)) {
        co_yield line;
    }
}

// 사용: 10GB 로그 파일도 한 줄씩만 메모리에
for (const auto& line : readLines("huge.log")) {
    if (line.find("ERROR") != std::string::npos) {
        processError(line);
    }
}

CSV 파싱 스트리밍

Generator<std::vector<std::string>> parseCsv(const std::string& path) {
    std::ifstream f(path);
    std::string line;
    while (std::getline(f, line)) {
        std::vector<std::string> row;
        // 간단한 CSV 파싱 (쉼표 구분)
        size_t pos = 0;
        while (pos < line.size()) {
            auto end = line.find(',', pos);
            if (end == std::string::npos) end = line.size();
            row.push_back(line.substr(pos, end - pos));
            pos = end == line.size() ? end : end + 1;
        }
        co_yield std::move(row);
    }
}

네트워크 청크 스트리밍

// 개념적 예: 청크 단위로 데이터 수신
Generator<std::vector<std::byte>> receiveChunks(Socket& sock, size_t chunk_size) {
    std::vector<std::byte> buffer(chunk_size);
    while (true) {
        size_t n = sock.read(buffer.data(), chunk_size);
        if (n == 0) break;
        buffer.resize(n);
        co_yield std::move(buffer);
        buffer.resize(chunk_size);
    }
}

배치 처리 파이프라인

// 로그 파일 → 필터(ERROR만) → 파싱 → 상위 100개
auto lines = readLines("app.log");
auto errors = filter(std::move(lines),
     {
        return s.find("ERROR") != std::string::npos;
    });
auto parsed = map(std::move(errors), parseLogLine);

int count = 0;
for (auto& entry : parsed) {
    handleError(entry);
    if (++count >= 100) break;
}

에러 처리·재시도·메트릭

  • 예외: readLines에서 throw → 호출 측 try/catch로 처리
  • 재시도: max_retries만큼 파일 열기 재시도 후 sleep_for 대기
  • 백프레셔: Generator는 pull 기반 — next() 호출 시에만 생산되므로 자연스럽게 적용
  • 메트릭: withMetrics(gen, count)count.fetch_add(1) 래핑
  • 배치: sliding_window(lines, 100)으로 100개씩 묶어 처리
  • 에러 복구: tryParse 실패 시 logWarning 후 건너뛰기

체크리스트

  • 파일 열기 실패 시 예외 또는 std::optional 처리
  • Generator가 파일/스트림을 소유할 때 수명 관리 확인
  • 대용량 시퀀스는 take로 상한 두기
  • string_view 대신 string 등 값 타입 사용 (lifetime 안전)
  • 불안정한 I/O는 재시도 로직 추가
  • 프로덕션에서는 처리량 메트릭 수집 고려
  • 파싱 실패 시 건너뛰기 vs 종료 정책 결정

10. 정리

항목내용
문제벡터는 전체 할당 → 메모리·계산 낭비
Generatorco_yield로 값 하나씩 lazy 생성
구현promise_type (yield_value, initial/final_suspend) + iterator
무한break/개수 제한으로 조절
조합filter, map, take, enumerate, sliding_window, flatten, distinct
lifetime참조 대신 값, 이동 후 원본 사용 금지
오류dangling 참조, 재귀 스택 오버플로우, 반복 begin() 금지
성능작은 타입·이동 활용, 파이프라인 단계 최소화, LTO
실전파일 라인별 읽기, CSV, 재시도, 메트릭, 배치 처리

같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.

  • C++20 Coroutine | co_await·co_yield로 “콜백 지옥” 탈출하기
  • C++ 비동기 작업과 Coroutine | co_await로 콜백 지옥 탈출하기 [#23-3]
  • C++ 캐시 최적화 | 메모리 접근 패턴 바꿔서 성능 10배 향상시키기

이 글에서 다루는 키워드 (관련 검색어)

C++ Generator, co_yield, lazy 시퀀스, 제너레이터, promise_type, 코루틴 제너레이터, 메모리 최적화 등으로 검색하시면 이 글이 도움이 됩니다.

자주 묻는 질문 (FAQ)

Q. C++23 std::generator와 차이는?

A. C++23 std::generator는 표준 라이브러리에 포함된 제너레이터 타입입니다. yield_value로 참조를 반환할 수 있어 복사 비용을 줄일 수 있습니다. C++20 환경에서는 이 글의 구현을 사용하거나 cppcoro를 활용할 수 있습니다.

Q. Generator를 스레드에서 써도 되나요?

A. Generator 객체 자체는 스레드 안전하지 않습니다. 한 스레드에서 begin()/end()로 순회하는 동안 다른 스레드에서 같은 Generator를 사용하면 데이터 레이스가 발생합니다. 스레드마다 별도 Generator를 만들거나, 동기화를 추가해야 합니다.

Q. 예외가 발생하면 어떻게 되나요?

A. unhandled_exception()에서 std::current_exception()을 저장하고, operator++()begin()에서 std::rethrow_exception으로 호출자에게 전파합니다. 호출하는 쪽에서 try/catch로 처리할 수 있습니다.

한 줄 요약: co_yield로 lazy 시퀀스를 만들면 메모리와 계산을 절약할 수 있습니다. Generator 구현, 무한 시퀀스, 조합, lifetime 주의사항, 성능 비교, 파일·스트리밍 패턴까지 실전에 바로 쓸 수 있습니다.

이전 글: C++ 코루틴 기초 (#23-1)

다음 글: C++ 비동기 코루틴 (#23-3)


관련 글

  • C++20 Coroutine | co_await·co_yield로
  • C++ 비동기 작업과 Coroutine | co_await로 콜백 지옥 탈출하기 [#23-3]
  • C++20 Concepts | 템플릿 에러 메시지를 읽기 쉽게 만드는 방법
  • C++ 커스텀 Concepts 작성 | 도메인에 맞는 제약 조건 정의하기 [#22-2]
  • C++20 Modules |