C++20 코루틴은 함수의 실행을 일시 중단하고 필요할 때 다시 재개할 수 있는 기능을 제공한다. 이를 활용하면 값을 하나씩 순차적으로 생성하는 Generator를 직접 구현할 수 있다. 이번 글에서는 Generator가 왜 필요한지, 어떤 원리로 동작하는지, promise_type과 co_yield가 어떻게 연관되는지를 자세히 다루겠다.
기존 반복자나 컨테이너 기반 접근 방식에는 몇 가지 한계가 있다. 먼저 모든 데이터를 한 번에 메모리에 올려야 하므로 메모리 효율이 떨어진다. 또한 피보나치 수열과 같은 무한 시퀀스를 구현하기 어렵다. 반복자를 직접 구현할 때는 operator++, operator*, end() 등을 일일이 정의해야 하므로 코드가 복잡해지고 유지보수가 어려워진다. Generator는 이러한 한계를 극복할 수 있으며, 필요한 순간에만 값을 생성하는 lazy evaluation을 지원한다.
Generator는 코루틴을 기반으로 한 구조로, 값 하나를 산출하고(co_yield) 일시 중단했다가 필요할 때 다시 재개할 수 있는 함수이다. 함수 내부에서 co_yield를 사용하면 실행이 일시 중단되고, 외부 호출자는 다시 resume을 호출하여 다음 값을 얻을 수 있다. Python의 generator와 유사하지만 C++에서는 직접 코루틴을 구현해야 한다는 점이 차이점이다.
C++20 코루틴 함수는 co_yield, co_return, co_await 키워드를 사용하면 컴파일러가 내부적으로 코루틴 프레임을 생성한다. 코루틴 프레임은 함수의 상태, 지역 변수, 프로그램 카운터 등을 저장한다. 코루틴과 호출자 간의 인터페이스는 promise_type이 담당하며, co_yield는 yield_value, co_return은 return_void, 예외는 unhandled_exception으로 처리된다. Generator는 promise_type, coroutine_handle, iterator 세 가지 요소가 조합되어 동작한다.
아래는 range-for까지 지원하는 최소한의 Generator 구현 예제이다. promise_type을 통해 co_yield를 처리하고, iterator를 통해 for-range 문에서 값을 하나씩 받을 수 있다.
#include <coroutine>
#include <optional>
#include <iostream>
template<typename T>
class Generator {
public:
struct promise_type {
std::optional<T> current_value;
Generator get_return_object() {
return Generator{ std::coroutine_handle<promise_type>::from_promise(*this) };
}
std::suspend_always initial_suspend() { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
std::suspend_always yield_value(T value) {
current_value = std::move(value);
return {};
}
void return_void() {}
void unhandled_exception() { std::exit(1); }
};
using handle_type = std::coroutine_handle<promise_type>;
explicit Generator(handle_type h) : coro(h) {}
Generator(const Generator&) = delete;
Generator& operator=(const Generator&) = delete;
Generator(Generator&& other) noexcept : coro(other.coro) { other.coro = nullptr; }
~Generator() { if (coro) coro.destroy(); }
class iterator {
public:
iterator() : coro(nullptr), done(true) {}
iterator(handle_type h) : coro(h) { advance(); }
iterator& operator++() { advance(); return *this; }
const T& operator*() const { return *coro.promise().current_value; }
bool operator==(std::default_sentinel_t) const { return done; }
private:
void advance() {
if (!coro) { done = true; return; }
coro.resume();
done = coro.done();
}
handle_type coro;
bool done;
};
iterator begin() { return iterator{ coro }; }
std::default_sentinel_t end() { return {}; }
private:
handle_type coro;
};
Generator를 사용하는 예제로는 1부터 n까지의 숫자를 순차적으로 생성하는 counter 함수를 들 수 있다. 함수 내부에서 co_yield를 사용하여 값을 하나씩 반환하고, 호출자는 for-range 문을 통해 자연스럽게 값을 받아올 수 있다.
Generator<int> counter(int n) {
for (int i = 1; i <= n; i++) {
co_yield i;
}
}
int main() {
for (int value : counter(5)) {
std::cout << value << " ";
}
std::cout << "\n";
}
이 코드를 실행하면 1 2 3 4 5가 출력된다. 중요한 점은 co_yield가 호출될 때 promise_type의 yield_value가 실행되며 값이 current_value에 저장된다는 것이다. yield_value는 suspend_always를 반환하므로 호출자가 resume을 호출해야 코루틴이 다음 단계로 진행된다. initial_suspend과 final_suspend를 통해 range-for 문에서 안전하게 첫 번째 값과 마지막 값을 처리할 수 있다.
Generator를 활용하면 피보나치 수열과 같은 무한 시퀀스, 파일이나 네트워크 스트리밍 데이터 처리, 참조 기반 Generator를 통한 복사 비용 최소화, 외부 Cancel/Stop 토큰과 연동한 중단 기능 등을 구현할 수 있다.
C++20 코루틴 기반 Generator는 기존 반복자나 컨테이너 한계의 보완 수단이 되며, lazy evaluation과 자연스러운 반복자 사용을 지원한다. promise_type의 역할을 이해하면 co_yield가 호출되는 순간의 처리 흐름과 호출자와 코루틴 간 인터페이스를 명확히 이해할 수 있다. 표준 라이브러리에는 아직 포함되지 않았지만 직접 구현이 가능하며, 다양한 응용과 확장이 가능하다. Generator를 이해하면 코루틴 기반 상위 레벨 패턴을 학습하는 좋은 기반이 된다.
'잡(job)기술' 카테고리의 다른 글
| I/O 기반 코루틴: 블로킹을 피하는 우아한 방법 (0) | 2025.09.04 |
|---|---|
| C++20 코루틴과 awaitable task (0) | 2025.09.03 |
| CMake --preset: 더 이상 길고 복잡한 빌드 명령어에 시달리지 말자 (1) | 2025.06.28 |
| Poetry로 Python 프로젝트 환경 만들기 (2) | 2025.06.24 |
| pyproject.toml 시작하기: Python 프로젝트의 현대적 설정 파일 (2) | 2025.06.24 |