I got a stack overflow exception in visual studio inside a coroutine in a loop and found that the loop had a bug that prevented it from termination but I wondered why the stack was overflowed ? the coroutine might not even was using the stack but the heap instead and even if the stack was used there was no any recursive calls at all
after some experiments I could reproduce the crash with :
msvc 19.28
g++-10 on wsl and mingw64
clang-cl 10 on windows and clang++-10 on linux
this code causes the stack overflow:
#include <stdexcept>
#include <utility>
#include <cstdio>
#ifdef __clang__
#ifdef _WIN32
#pragma message "using clang coroutine header"
#include "clang-cl-coro.h"
#else
#pragma message "using coroutine experimental header"
#include <experimental/coroutine>
#endif
namespace std
{
template<class P = void>
using coroutine_handle = experimental::coroutine_handle<P>;
using suspend_never = experimental::suspend_never;
using suspend_always = experimental::suspend_always;
}
#else
#pragma message "using coroutine header"
#include <coroutine>
#endif
class vtask
{
inline static size_t task_count = 0;
public:
struct promise_type
{
inline static size_t promise_count = 0;
std::coroutine_handle<> waiter;
std::exception_ptr ex_ptr = nullptr;
struct resume_waiter
{
inline static size_t awaiter_count = 0;
std::coroutine_handle<> waiter;
resume_waiter(std::coroutine_handle<> waiter) noexcept : waiter{ waiter }
{
++awaiter_count;
printf("[%zu] resume_waiter(std::coroutine_handle<> waiter)\n", awaiter_count);
}
~resume_waiter()
{
--awaiter_count;
printf("[%zu] ~resume_waiter()\n", awaiter_count);
}
bool await_ready() const noexcept { return false; }
auto await_suspend(std::coroutine_handle<>) noexcept
{
return waiter;
}
void await_resume() const noexcept {}
};
promise_type()
{
++promise_count;
printf("[%zu] vtask::promise_type()\n", promise_count);
}
~promise_type()
{
--promise_count;
printf("[%zu] ~vtask::promise_type()\n", promise_count);
}
vtask get_return_object() { return { *this }; }
constexpr std::suspend_always initial_suspend() noexcept { return {}; }
resume_waiter final_suspend() const noexcept { return { waiter }; }
void unhandled_exception() noexcept
{
ex_ptr = std::current_exception();
}
void return_void() const noexcept {}
};
vtask(promise_type& p) : coro{ std::coroutine_handle<promise_type>::from_promise(p) }
{
++task_count;
printf("[%zu] vtask(promise_type& p)\n", task_count);
}
vtask(vtask&& other) noexcept : coro{ std::exchange(other.coro, nullptr) }
{
++task_count;
printf("[%zu] vtask(vtask&& other)\n", task_count);
}
~vtask()
{
if (coro)
coro.destroy();
--task_count;
printf("[%zu] ~vtask()\n", task_count);
}
bool await_ready() const noexcept
{
return false;
}
void await_suspend(std::coroutine_handle<> waiter)
{
coro.promise().waiter = waiter;
coro.resume();
}
void await_resume() noexcept {}
private:
std::coroutine_handle<promise_type> coro;
};
struct detached_task
{
struct promise_type
{
inline static size_t promise_count = 0;
promise_type()
{
++promise_count;
printf("[%zu] detached_task::promise_type()\n", promise_count);
}
~promise_type()
{
--promise_count;
printf("[%zu] ~detached_task::promise_type()\n", promise_count);
}
detached_task get_return_object() { return {}; }
std::suspend_never initial_suspend() noexcept { return {}; }
std::suspend_never final_suspend() noexcept { return {}; }
void unhandled_exception() noexcept
{
std::terminate();
}
constexpr void return_void() const noexcept {}
};
inline static size_t task_count = 0;
detached_task()
{
++task_count;
printf("[%zu] detached_task()\n", task_count);
}
~detached_task()
{
--task_count;
printf("[%zu] ~detached_task()\n", task_count);
}
};
vtask do_stackoverflow() { co_return; }
detached_task stackoverflow()
{
for (;;)
co_await do_stackoverflow();
}
int main()
{
stackoverflow();
}
command lines used:
cl /std:c++latest coro-stackoverflow.cpp /EHsc for msvc
g++ -std=c++20 coro-stackoverflow.cpp -fcoroutines for mingw64
clang-cl /std:c++latest coro-stackoverflow.cpp /EHsc
g++-10 -std=c++20 coro-stackoverflow.cpp -fcoroutines -o overflow.bug on wsl
clang++-10 -std=c++20 -stdlib=libc++ coro-stackoverflow.cpp -o overflow-clang.bug on wsl
and this is the clang coro header on windows:
#pragma once
namespace std { namespace experimental { inline namespace coroutines_v1 {
template <typename R, typename...> struct coroutine_traits {
using promise_type = typename R::promise_type;
};
template <typename Promise = void> struct coroutine_handle;
template <> struct coroutine_handle<void> {
static coroutine_handle from_address(void *addr) noexcept {
coroutine_handle me;
me.ptr = addr;
return me;
}
void operator()() { resume(); }
void *address() const { return ptr; }
void resume() const { __builtin_coro_resume(ptr); }
void destroy() const { __builtin_coro_destroy(ptr); }
bool done() const { return __builtin_coro_done(ptr); }
coroutine_handle &operator=(decltype(nullptr)) {
ptr = nullptr;
return *this;
}
coroutine_handle(decltype(nullptr)) : ptr(nullptr) {}
coroutine_handle() : ptr(nullptr) {}
// void reset() { ptr = nullptr; } // add to P0057?
explicit operator bool() const { return ptr; }
protected:
void *ptr;
};
template <typename Promise> struct coroutine_handle : coroutine_handle<> {
using coroutine_handle<>::operator=;
static coroutine_handle from_address(void *addr) noexcept {
coroutine_handle me;
me.ptr = addr;
return me;
}
Promise &promise() const {
return *reinterpret_cast<Promise *>(
__builtin_coro_promise(ptr, alignof(Promise), false));
}
static coroutine_handle from_promise(Promise &promise) {
coroutine_handle p;
p.ptr = __builtin_coro_promise(&promise, alignof(Promise), true);
return p;
}
};
template <typename _PromiseT>
bool operator==(coroutine_handle<_PromiseT> const& _Left,
coroutine_handle<_PromiseT> const& _Right) noexcept
{
return _Left.address() == _Right.address();
}
template <typename _PromiseT>
bool operator!=(coroutine_handle<_PromiseT> const& _Left,
coroutine_handle<_PromiseT> const& _Right) noexcept
{
return !(_Left == _Right);
}
template <typename _PromiseT>
bool operator==(coroutine_handle<_PromiseT> const& _Left,
std::nullptr_t) noexcept
{
return _Left.address() == nullptr;
}
template <typename _PromiseT>
bool operator==(std::nullptr_t, coroutine_handle<_PromiseT> const& _Right) noexcept
{
return _Right.address() == nullptr;
}
template <typename _PromiseT>
bool operator!=(coroutine_handle<_PromiseT> const& _Left,
std::nullptr_t) noexcept
{
return !(_Left == nullptr);
}
template <typename _PromiseT>
bool operator!=(std::nullptr_t, coroutine_handle<_PromiseT> const& _Right) noexcept
{
return _Right.address() != nullptr;
}
struct suspend_always {
bool await_ready() { return false; }
void await_suspend(coroutine_handle<>) {}
void await_resume() {}
};
struct suspend_never {
bool await_ready() { return true; }
void await_suspend(coroutine_handle<>) {}
void await_resume() {}
};
}}}
on windows the crash occurs early for msvc and clang-cl builds but mingw64 and wsl builds takes more time
gcc 10.1 which I used seems to have a knows bug where the task is constructed twice but destroyed once leaking a task each iteration which seems to cause the overflow
but clang and msvc don't have this bug and they also crash !
Edit: tried gcc 10.3 mingw64 and it doesn't have the mentioned gcc bug but it also cause a stack overflow ! even faster the the bugged compiler ! maybe this behavior is expected ?
I can't figure out which is wrong with the code above
The problem was in this part of code :
void await_suspend(std::coroutine_handle<> waiter)
{
coro.promise().waiter = waiter;
coro.resume();
}
this asymmetric transfer caused the stackoverflow and changing the code to :
auto await_suspend(std::coroutine_handle<> waiter)
{
coro.promise().waiter = waiter;
return coro;
}
eliminates the problem on clang and msvc but gcc 10.3 still crashes, I think it doesn't support symmetric transfer yet
Related
I'm trying to implement a stackful coroutine system in C++20, based on this SO post.
The problem with the linked code is that it works only if the nested coroutines are of the same task<T> type, so I'm trying to extend it in order to be possible to co_await a task with any T. In other words, it's OK the return object always being a task<T>, but the awaiting coroutine return object's T can be different from the awaited coroutine return object's T.
This is my code so far:
template<class T>
class task {
public:
class promise_type {
protected:
T value;
std::coroutine_handle<> innerHandler{};
std::coroutine_handle<> outerHandler{};
friend class task;
public:
task get_return_object() {
return task(std::coroutine_handle<promise_type>::from_promise(*this));
}
auto initial_suspend() {
return std::suspend_never{};
}
auto final_suspend() noexcept {
return std::suspend_always{};
}
auto return_value(T v) {
this->value = std::move(v);
return std::suspend_always{};
}
void unhandled_exception() {
abort("Unhandled exception in Coroutine");
}
};
explicit task(std::coroutine_handle<promise_type> handle) : handle(handle) {}
task(const task&) = delete;
task(task&& c) noexcept : handle(std::exchange(c.handle, nullptr)) {}
task& operator=(const task&) = delete;
task& operator=(task&& c) noexcept {
this->handle = std::exchange(c.handle, nullptr);
return *this;
}
~task() {
if (this->handle) {
this->handle.destroy();
}
}
constexpr bool await_ready() const noexcept {
return !this->handle || this->handle.done();
}
bool await_suspend(std::coroutine_handle<> h) {
h.promise().innerHandler = this->handle;
this->handle.promise().outerHandler = h;
return true;
}
constexpr T await_resume() const noexcept {
return std::move(this->handle.promise().value);
}
bool next() {
auto cur = this->handle;
while (cur) {
if (!cur.promise().innerHandler) {
while (!cur.done()) {
cur.resume();
if (!cur.done()) {
return true;
}
if (cur.promise().outerHandler) {
cur = cur.promise().outerHandler;
cur.promise().innerHandler = nullptr;
}
else {
return false;
}
}
break;
}
cur = cur.promise().innerHandler;
}
return !cur.done();
}
private:
std::coroutine_handle<promise_type> handle;
};
The code is failing to compile on h.promise().innerHandler = this->handle; in bool await_suspend(std::coroutine_handle<> h), because std::coroutine_handle<> doesn't expose the promise() member. The code from the linked post works because OP declares h as std::coroutine_handle<promise_type>, but I can't use it or else I'm stuck again at co_awaiting return types with the same T.
I've also tried using await_trasform() but it is even more difficult to understand than what I'm already struggling with, using these C++ coroutines.
How could I implement this?
So in the end I managed to find a way to do it.
Basically turns out that with std::coroutine_handle::from_address() I can cast a promise type-deleted std::coroutine_handle<> to one specific to my promise_type, like so:
// With h being a std::coroutine_handle<>
auto cH = std::coroutine_handle<promise_type>::from_address(h.address());
There isn't any compile-time check in place to make sure that h is actually castable to std::coroutine_handle<promise_type>, and I think there are some edge cases that require them, but at least for my application I can safely make this assumption.
This is the complete code for class task:
template<class T>
class task {
public:
class promise_type {
protected:
std::unique_ptr<T> value;
std::coroutine_handle<promise_type> innerHandler{};
std::coroutine_handle<promise_type> outerHandler{};
friend class task;
public:
task get_return_object() {
return task(std::coroutine_handle<promise_type>::from_promise(*this));
}
auto initial_suspend() {
return std::suspend_never{};
}
auto final_suspend() noexcept {
return std::suspend_always{};
}
auto return_value(T v) {
this->value = std::make_unique<T>(std::move(v));
return std::suspend_always{};
}
void unhandled_exception() {
abort("Unhandled exception in Coroutine");
}
};
explicit task(std::coroutine_handle<promise_type> handle) : handle(handle) {}
task(const task&) = delete;
task(task&& c) noexcept : handle(std::exchange(c.handle, nullptr)) {}
task& operator=(const task&) = delete;
task& operator=(task&& c) noexcept {
this->handle = std::exchange(c.handle, nullptr);
return *this;
}
~task() {
if (this->handle) {
this->handle.destroy();
}
}
constexpr bool await_ready() const noexcept {
return !this->handle || this->handle.done();
}
bool await_suspend(std::coroutine_handle<> h) {
auto cH = std::coroutine_handle<promise_type>::from_address(h.address());
cH.promise().innerHandler = this->handle;
this->handle.promise().outerHandler = cH;
return true;
}
constexpr T await_resume() const noexcept {
return std::move(*this->handle.promise().value);
}
bool next() {
auto cur = this->handle;
while (cur) {
if (!cur.promise().innerHandler) {
while (!cur.done()) {
cur.resume();
if (!cur.done()) {
return true;
}
if (cur.promise().outerHandler) {
cur = cur.promise().outerHandler;
cur.promise().innerHandler = nullptr;
}
else {
return false;
}
}
break;
}
cur = cur.promise().innerHandler;
}
return !cur.done();
}
private:
std::coroutine_handle<promise_type> handle;
};
When calling co_await request(xxx) in a coroutine the GCC compiler asked for move ctor even if the request function returned lvalue reference. Is this a bug? Seems this behavior is not compliant with the standard. Checked with clang and it compiles.
simple code snippets:
#include <coroutine>
using namespace std;
struct awaitable {
awaitable() = default;
awaitable(const awaitable&) = delete;
awaitable(const awaitable&&) = delete;
bool await_ready() { return false; }
void await_suspend(coroutine_handle<> h) { }
void await_resume() {}
};
awaitable& request();
struct task{
struct promise_type {
task get_return_object() { return {}; }
suspend_never initial_suspend() { return {}; }
suspend_never final_suspend() noexcept { return {}; }
void return_void() {}
void unhandled_exception() {}
};
};
task test_coroutine() {
co_await request();
}
compile with g++-11 -c -std=c++20 test.cxx gives
test.cxx: In function ‘task test_coroutine()’:
test.cxx:28:22: error: use of deleted function ‘awaitable::awaitable(const awaitable&&)’
28 | co_await request();
| ^
test.cxx:8:5: note: declared here
8 | awaitable(const awaitable&&) = delete;
| ^~~~~~~~~
A few years ago i had my Open tibia server, it's old source engine, previous compiled at gcc 4.7+, already i try to compile it at newest debian, my gcc version is: 8.3.0 (Debian 8.3.0-6)
error: no match for call to ‘(Task) (Game*&)’
(*_f2)(arg);
error displays: expression cannot be used as a function
(_f2)(arg);
template<class ArgType>
class TCallList : public SchedulerTask{
public:
TCallList(
boost::function<bool(Game*, ArgType)> f1,
Task* f2,
std::list<ArgType>& call_list,
int64_t interval) :
_f1(f1), _f2(f2), _list(call_list), _interval(interval)
{
//
}
virtual void operator()(Game* arg)
{
if(_eventid != 0){
bool ret = _f1(arg, _list.front());
_list.pop_front();
if(ret){
if(_list.empty()){
//callback function
if(_f2){
(_f2)(arg);
delete _f2;
}
}
else{
//fire next task
SchedulerTask* newTask = new TCallList(_f1, _f2, _list, _interval);
newTask->setTicks(_interval);
newTask->setEventId(this->getEventId());
arg->addEvent(newTask);
}
}
}
}
private:
boost::function<bool(Game*, ArgType)> _f1;
Task* _f2;
std::list<ArgType> _list;
int64_t _interval;
};
template<class ArgType>
SchedulerTask* makeTask(int64_t ticks,
boost::function<bool(Game*, ArgType)>* f1,
std::list<ArgType>& call_list,
int64_t interval,
Task* f2)
{
TCallList<ArgType>* t = new TCallList<ArgType>(f1, f2, call_list, interval);
t->setTicks(ticks);
return t;
}
Scheduler.cpp
#ifndef __OTSERV_SCHEDULER_H__
#define __OTSERV_SCHEDULER_H__
#include <boost/function.hpp>
#include <boost/bind.hpp>
#include <functional>
#include "otsystem.h"
class Game;
class SchedulerTask {
public:
SchedulerTask() {
_eventid = 0;
_cycle = 0;
}
virtual ~SchedulerTask()
{
//
}
// definition to make sure lower cycles end up front
// in the priority_queue used in the scheduler
inline bool operator<(const SchedulerTask& other) const
{
return getCycle() > other.getCycle();
}
virtual void operator()(Game* arg) = 0;
virtual void setEventId(uint32_t id)
{
_eventid = id;
}
inline uint32_t getEventId() const
{
return _eventid;
}
virtual void setTicks(const int64_t ticks)
{
_cycle = OTSYS_TIME() + ticks;
}
inline int64_t getCycle() const
{
return _cycle;
}
protected:
uint32_t _eventid;
int64_t _cycle;
};
class TSchedulerTask : public SchedulerTask{
public:
TSchedulerTask(boost::function1<void, Game*> f) :
_f(f)
{
//
}
virtual ~TSchedulerTask()
{
//
}
virtual void operator()(Game* arg)
{
_f(arg);
}
protected:
boost::function1<void, Game*> _f;
};
SchedulerTask* makeTask(boost::function1<void, Game*> f);
SchedulerTask* makeTask(int64_t ticks, boost::function1<void, Game*> f);
class lessSchedTask : public std::binary_function<SchedulerTask*, SchedulerTask*, bool>{
public:
bool operator()(SchedulerTask*& t1, SchedulerTask*& t2)
{
return *t1 < *t2;
}
};
#endif
I've created a class called zgenerator:
template <typename T>
class zgenerator {
public:
class promise_type;
using handle = std::coroutine_handle<promise_type>;
private:
handle coro;
explicit zgenerator(handle h) : coro{h} {}
public:
// default constructor
zgenerator() noexcept : coro{nullptr} {}
// move constructor and assignment
zgenerator(zgenerator&& other) noexcept : coro{std::move(other.coro)} { }
zgenerator& operator=(zgenerator&& other) noexcept {
coro = std::move(other.coro);
return *this;
}
// deleting copy constructor and assignment
zgenerator(const zgenerator&) = delete;
zgenerator& operator=(const zgenerator&) = delete;
// destructor
~zgenerator() { if (coro) coro.destroy(); }
// promise type
class promise_type {
public:
using value_type = std::remove_reference_t<T>;
using reference_type = std::conditional_t<std::is_reference_v<T>, T, T&>;
using pointer_type = value_type*;
private:
pointer_type m_value;
std::exception_ptr m_except;
public:
zgenerator get_return_object() noexcept {
return zgenerator{handle::from_promise(*this)};
}
static auto get_return_object_on_allocation_failure() {
return zgenerator{nullptr};
}
std::suspend_always initial_suspend() const noexcept { return {}; }
std::suspend_always final_suspend() const noexcept { return {}; }
// no 'co_await'
template <typename T1> std::suspend_never await_transform(T1&&) = delete;
std::suspend_always yield_value(std::remove_reference_t<T>& value) noexcept
requires std::is_rvalue_reference_v<T>
{
m_value = std::addressof(value);
return {};
}
std::suspend_always yield_value(std::remove_reference_t<T>&& value) noexcept {
m_value = std::addressof(value);
return {};
}
void unhandled_exception() {
m_except = std::current_exception();
}
void rethrow_if_exception() {
if (m_except)
std::rethrow_exception(m_except);
}
void return_void() {}
reference_type value() const noexcept {
return static_cast<reference_type>(*m_value);
}
};
// sentinel
struct sentinel {};
// iterator
class iterator {
private:
handle m_coro;
public:
using value_type = typename promise_type::value_type;
using reference = typename promise_type::reference_type;
using pointer = typename promise_type::pointer_type;
using difference_type = std::ptrdiff_t;
using iterator_category = std::input_iterator_tag;
iterator() noexcept : m_coro{nullptr} {}
explicit iterator(handle other) noexcept : m_coro{other} {}
friend bool operator==(const iterator& it, sentinel) noexcept {
return !it.m_coro || it.m_coro.done();
}
friend bool operator==(sentinel s, const iterator& it) noexcept {
return (it == s);
}
iterator& operator++() {
m_coro.resume();
if (m_coro.done()) {
m_coro.promise().rethrow_if_exception();
}
return *this;
}
void operator++(int) {
(void) this->operator++();
}
reference operator*() const noexcept {
return m_coro.promise().value();
}
pointer operator->() const noexcept {
return std::addressof(this->operator*());
}
};
// iterators
iterator begin() {
if (coro) {
coro.resume();
if (coro.done()) {
coro.promise().rethrow_if_exception();
}
}
return iterator{coro};
}
sentinel end() noexcept {
return sentinel{};
}
void swap(zgenerator& other) noexcept {
using std::swap;
swap(coro, other.coro);
}
// manual yielding
T value() { return coro.promise().value(); }
bool next() {
if (coro) {
coro.resume();
return !coro.done();
} else
return false;
}
bool is_done() { return coro.done(); }
};
This is the application:
auto generate_num = [] -> zgenerator<int> {
co_yield 1;
co_yield 3;
co_yield 10;
co_yield -1;
};
auto it = generate_num();
// Used in for-loop:
for (const auto& i : it)
std::cout << i << ' ';
std::cout << '\n';
Output:
1 3 10 -1
I want to reset the object back into the first state. When I tried to reassign a new generator object from it, it throws a runtime error:
it = generate_num();
Expectation:
// reset:
it.reset();
it.next();
while (!it.is_done()) {
std::cout << it.value() << ' ';
it.next();
}
// now printing twice if we include this.
How could I implement a resetting function in my generator?
Your move constructor and assignment need to ensure other.coro doesn't still point to what it used to, otherwise you double-destroy it.
zgenerator(zgenerator&& other) noexcept : coro{std::exchange(other.coro, {})} { }
zgenerator& operator=(zgenerator&& other) noexcept {
std::swap(coro, other.coro);
return *this;
}
So, there I'm trying to comprehend that new & complex concept of the coroutines. Took Clang for it, compiling via clang++ -std=c++17 -fcoroutines-ts -stdlib=libc++ goes fine.
One of the most useful concepts is task<> coroutine type, it's mentioned here and even has a couple interesting implementations, by Gor Nishanov and in cppcoro library.
Okay, looked fine to try myself in the simpliest case. So, the goal is to implement something that should work like the following:
{
auto producer = []() -> task<int> {
co_return 1;
};
auto t = producer();
assert(!t.await_ready());
assert(t.result() == 1);
assert(t.await_ready());
}
The template class task<> itself was made quite straightforward:
#pragma once
#include <experimental/coroutine>
#include <optional>
namespace stdx = std::experimental;
template <typename T=void>
struct task
{
template<typename U>
struct task_promise;
using promise_type = task_promise<T>;
using handle_type = stdx::coroutine_handle<promise_type>;
mutable handle_type m_handle;
task(handle_type handle)
: m_handle(handle)
{}
task(task&& other) noexcept
: m_handle(other.m_handle)
{ other.m_handle = nullptr; };
bool await_ready()
{ return m_handle.done(); }
bool await_suspend(stdx::coroutine_handle<> handle)
{
if (!m_handle.done()) {
m_handle.resume();
}
return false;
}
auto await_resume()
{ return result(); }
T result() const
{
if (!m_handle.done())
m_handle.resume();
if (m_handle.promise().m_exception)
std::rethrow_exception(m_handle.promise().m_exception);
return *m_handle.promise().m_value;
}
~task()
{
if (m_handle)
m_handle.destroy();
}
template<typename U>
struct task_promise
{
std::optional<T> m_value {};
std::exception_ptr m_exception = nullptr;
auto initial_suspend()
{ return stdx::suspend_always{}; }
auto final_suspend()
{ return stdx::suspend_always{}; }
auto return_value(T t)
{
m_value = t;
return stdx::suspend_always{};
}
task<T> get_return_object()
{ return {handle_type::from_promise(*this)}; }
void unhandled_exception()
{ m_exception = std::current_exception(); }
void rethrow_if_unhandled_exception()
{
if (m_exception)
std::rethrow_exception(std::move(m_exception));
}
};
};
Couldn't really make a smaller piece of code complete and compilable, sorry. Anyway it worked somehow, but there still remained the case of task<void>, it's usage could be like the following:
{
int result = 0;
auto int_producer = []() -> task<int> {
co_return 1;
};
auto awaiter = [&]() -> task<> { // here problems begin
auto i1 = co_await int_producer();
auto i2 = co_await int_producer();
result = i1 + i2;
};
auto t = awaiter();
assert(!t.await_ready());
t.await_resume();
assert(result == 2);
}
The latter didn't seem a problem at all, it looked like task_promise<U> required a specialization for void (could be a non-template struct without that void case). So, I tried it:
template<>
struct task_promise<void>
{
std::exception_ptr m_exception;
void return_void() noexcept {}
task<void> get_return_object() noexcept
{ return {handle_type::from_promise(*this)}; }
void unhandled_exception()
{ m_exception = std::current_exception(); }
auto initial_suspend()
{ return stdx::suspend_always{}; }
auto final_suspend()
{ return stdx::suspend_always{}; }
};
Neat and simple... and it causes segfault without any readable stacktrace =(
Works fine when task<> is changed to any non-void template, like task<char>.
What is wrong with my template specialization? Or am I missing some tricky concept with those coroutines?
Would be grateful for any ideas.
Apparently, the usual suspect was the criminal: specialization! From the standard itself [temp.expl.spec]/7
When writing a specialization, be careful about its location; or to make it compile will be such a trial as to kindle its self-immolation.
To avoid issue, let's make it as simple as possible: task_promise can be a non template and the member specialization is declared as soon as possible:
template<class T=void>
struct task{
//...
struct task_promise{
//...
};
};
//member specialization declared before instantiation of task<void>;
template<>
struct task<void>::task_promise{
//...
};