본문 바로가기

진리는어디에/C++

[C++20] 코루틴(Coroutine) - co_yield

이번 강의는 [C++20] 코루틴(Coroutine)에 이어지는 내용 입니다. 이번 강의를 읽으시기 전에 이전 포스팅을 먼저 읽어 보시길 추천 드립니다.

co_yield

코루틴을 중단(suspend)하고 호출자에게 돌아갈 때, 호출자에게 값을 넘기고 싶다면 co_await대신 co_yield를 사용 하면 됩니다. 

Generator foo()
{
    //co_await std::suspend_always{};
    co_yield 10;
}

하지만 컴파일러는 'co_yield' 구문을 만나면 내부적으로 다음과 같은 코드를 생성합니다.

Generator foo()
{
    Generator::promise_type promise;
    // ...코드 생략...
    // co_yield 10;
    co_await promise.yield_value(10);
}

위와 같이 co_yield 구문은 co_await promise.yield_value(expr) 와 같은 구문으로 컴파일러에 의해 변경 됩니다. 때문에, 단순히 co_await를 co_yield 키워드로만 바꾸고 컴파일 한다면 "promise_type 에서 yield_vaule() 함수를 찾을 수 없습니다"와 같은 오류를 보게 될것 입니다.

[이전 포스트] 에서 코루틴의 결과나 예외를 promise_type을 통해 호출자에게 돌려 준다고 이야기 했었습니다. co_yield를 사용하여 호출자에게 값을 전달하기 위해서는 promise_type에 전달 할 값을 저장할 멤버 변수yield_value()함수를 정의 해주셔야 합니다.

만일 코루틴이 int 타입을 호출자에게 리턴한다면 promise_type 구조체 선언에 멤버 변수로 int 타입 변수를 추가하고, 그 멤버 변수에 값을 저장하는 yield_value함수를 만들어 주셔야 합니다.

class Task
{
    struct promise_type
    {
        // 다른 코드들 생략
        int value;
        
        std::suspend_always yield_value(int value)
        {
            this->value = value;
            return {};
        }
    };
};

Generator foo()
{
    // co_yield 10;
    co_await promise.yield_value(10);
}

컴파일러가 생성한 코드에서 co_await는 yield_value()를 호출하여 promise에 리턴할 값을 저장하고, yield_value에서 리턴 되는 suspend_always에 의해 코루틴을 중단(suspend) 하고 호출자에게 돌아갑니다.

Task foo()
{
    int value = 10;
    std::cout << "foo 1" << " " << value << std::endl;
    
    co_yield value;
    
    value += 10;
    std::cout << "foo 2" << " " << value << std::endl;
}

int main()
{
    Task task = foo();
    task.co_handler.resume(); // start coroutine
    std::cout << "main 1" << " " << task.co_handler.promise().value << std::endl;
    task.co_handler.resume();
    std::cout << "main 2" << " " << task.co_handler.promise().value << std::endl;
}

코루틴에서 yield한 값을 메인함수에서 promise().value를 통해 전달 받아 출력 할 수 있는 것을 볼수 있습니다.

정리

  • 코루틴에서 호출자에게 값을 전달하기 위해서는 promise_type을 이용합니다.
  • promise_type은 전달 할 값을 저장할 멤버 변수와 yield_value라는 멤버 함수를 정의해야 합니다.

부록 1. 전체 코드

class Task
{
public:
    struct promise_type
    {
        int value; // 코루틴에서 호출자에게 리턴 할 값
        
        Task get_return_object() { return Task{ std::coroutine_handle<promise_type>::from_promise(*this) }; }
        auto initial_suspend() { return std::suspend_always{}; }
        
        // 코루틴에서 co_yield를 호출하기 위해 필요한 함수
        std::suspend_always yield_value(int v)
        {
            this->value = v;
            return {};
        }
        
        void return_void() { return; }
        auto final_suspend() noexcept { return std::suspend_always{}; }
        void unhandled_exception() { std::exit(1); }
    };
    
	std::coroutine_handle<promise_type> co_handler;
    Task(std::coroutine_handle<promise_type> handler) : co_handler(handler)
    {
    }
    ~Task()	{ if (co_handler) { co_handler.destroy(); }	}
};

Task foo()
{
    int value = 10;
    std::cout << "foo 1" << " " << value << std::endl;	
    
    co_yield value;
    
    value += 10;
    std::cout << "foo 2" << " " << value << std::endl;
}

int main()
{
    Task task = foo();
    task.co_handler.resume(); // start coroutine	
    std::cout << "main 1" << " " << task.co_handler.promise().value << std::endl;
    task.co_handler.resume();
    std::cout << "main 2" << " " << task.co_handler.promise().value << std::endl;
}

부록 2. 템플릿(template) 적용

템플릿을 적용하여 코루틴의 리턴 타입이 달라진다고 해도 매번 새로운 타입의 promise_type을 정의하는 것이 아니라 기존 클래스에 템플릿 인자만 다르게 사용하는 방식도 적용 가능합니다. 템플릿 특수화를 통해 템플릿 인자 T의 타입에 따라 다양한 promise_type을 컴파일 타임에 지정 해줄 수도 있습니다.

template <class T>
class Coroutine
{
private:
    struct promise_base
    {
        Coroutine get_return_object();
        {
            return Coroutine {};
        }
        
        INITIAL_SUSPEND initial_suspend()
        {
            return INITIAL_SUSPEND{};
        }
        
        std::suspend_always final_suspend() noexcept
        {
            return {};
        }
        
        void unhandled_exception()
        {
            throw std::exception("unhandled exception");
        }
        
        void return_void()
        {
            return;
        }
    };

    template <class R>
    struct promise_type_impl : public promise_base
    {
        R value;
        std::suspend_always yield_value(const R& value)
        {
            this->value = value;
            return {};
        }
    };
    
    template <>
    struct promise_type_impl<void> : public promise_base
    {
    };
public:
    typedef promise_type_impl<typename T> promise_type;
};

부록 3. 같이 보면 좋은 글

유익한 글이었다면 공감(❤) 버튼 꾹!! 추가 문의 사항은 댓글로!!