Skip to content

Commit b518d82

Browse files
committed
Ch-ch-ch-changes and improvements
* co_thread is much cleaner now. * Also it handles exceptions inside the thread and propagates them outside. * Added signal::connect overload that returns a std::unique_ptr<has_slots> for convenient RAII-based lifetime. * Wrote some words. Actual words.
1 parent 1e78eaa commit b518d82

6 files changed

Lines changed: 262 additions & 94 deletions

File tree

README

Lines changed: 0 additions & 16 deletions
This file was deleted.

README.md

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# sigslot - C++11 Signal/Slot library
2+
3+
Originally written by Sarah Thompson.
4+
5+
Various patches and fixes applied by Cat Nap Games:
6+
7+
To make this compile under Xcode 4.3 with Clang 3.0 I made some changes myself and also used some diffs published in the original project's Sourceforge forum.
8+
I don't remember which ones though.
9+
10+
C++11-erization (and C++2x-erixation, and mini coroutine library) by Dave Cridland:
11+
12+
See example.cc and co_example.cc for some documentation and a walk-through example, or read the tests.
13+
14+
This is public domain; no copyright is claimed or asserted.
15+
16+
No warranty is implied or offered either.
17+
18+
## Tagging and version
19+
20+
Until recently, I'd say just use HEAD. But some people are really keen on tags, so I'll do some semantic version tagging on this.
21+
22+
## Promising, yet oddly vague and sometimes outright misleading documentation
23+
24+
This library is a pure header library, and consists of four header files:
25+
26+
<sigslot/siglot.h>
27+
28+
This contains a sigslot::signal<T...> class, and a sigslot::has_slots class.
29+
30+
Signals can be connected to arbitrary functions, but in order to handle disconnect on lifetime termination, there's a "has_slots" base class to make it simpler.
31+
32+
Loosely, calling "emit(...)" on the signal will then call all the connected "slots", which are just arbitrary functions.
33+
34+
If a class is derived (publicly) from has_slots, you can pass in the instance of the class you want to control the lifetime. For calling a specific member directly, that's an easy decision; but if you pass in a lambda or some other arbitrary function, it might not be.
35+
36+
If there's nothing obvious to hand, something still needs to control the scope - leaving out the has_slots argument therefore returns you a (deliberately undocumented) placeholder class, which acts in lieu of a has_slots derived class of your choice.
37+
38+
<sigslot/tasklet.h>
39+
40+
This has a somewhat integrated coroutine library. Tasklets are coroutines, and like most coroutines they can be started, resumed, etc. There's no generator defined, just simple coroutines.
41+
42+
Tasklets expose co_await, so can be awaited by other coroutines. Signals can also be awaited upon, and will resolve to nothing (ie, void), or the single type, or a std::tuple of the types.
43+
44+
<sigslot/resume.h>
45+
46+
Coroutine resumption can be tricky, and is usually best integrated into some kind of event loop. Failure to do so will make it very hard to do anything that you couldn't do as well (or better!) without.
47+
48+
You can define your own resume function which will be called when a coroutine should be resumed, a trivial (and rather poor) example is at the beginning of the co_thread tests.
49+
50+
If you don't, then std::coroutine_handle<>::resume() will be called directly (which works for trivial cases, but not for anything useful).
51+
52+
<sigslot/cothread.h>
53+
54+
sigslot::co_thread is a convenient (but very simple) wrapper to run a non-coroutine in a std::jthread, but outwardly behave as a coroutine. Construct once, and it can be treated as a coroutine definition thereafter, and called multiple times.
55+
56+
This will not work with the built-in resumption, you'll need to implement *some* kind of event loop.

sigslot/cothread.h

Lines changed: 156 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -10,91 +10,193 @@
1010
#include "sigslot/tasklet.h"
1111

1212
namespace sigslot {
13-
template<typename Result, class... Args>
14-
class co_thread {
15-
public:
16-
// Single argument version uses a bare T
13+
namespace cothread_internal {
14+
template<typename Result>
1715
struct awaitable {
1816
std::coroutine_handle<> awaiting = nullptr;
19-
co_thread & wait_for;
2017

21-
explicit awaitable(co_thread & t) : wait_for(t) {}
18+
awaitable() = default;
19+
awaitable(awaitable && other) = delete;
20+
awaitable(awaitable const &) = delete;
2221

2322
bool await_ready() {
24-
return wait_for.has_payload();
23+
return has_payload();
2524
}
2625

2726
void await_suspend(std::coroutine_handle<> h) {
2827
// The awaiting coroutine is already suspended.
2928
awaiting = h;
30-
wait_for.await(this);
29+
await();
3130
}
3231

3332
auto await_resume() {
34-
return wait_for.payload();
33+
return payload();
3534
}
3635

3736
void resolve() {
3837
std::coroutine_handle<> a = nullptr;
3938
std::swap(a, awaiting);
4039
if (a) sigslot::resume_switch(a);
4140
}
41+
42+
template<typename Fn, typename ...Args>
43+
void run(Fn & fn, Args&&... args) {
44+
auto wrapped_fn = [this, &fn](Args... a) {
45+
try {
46+
auto result = fn(a...);
47+
{
48+
std::lock_guard l_(m_mutex);
49+
m_payload.emplace(result);
50+
resolve();
51+
}
52+
} catch(...) {
53+
std::lock_guard l_(m_mutex);
54+
m_eptr = std::current_exception();
55+
resolve();
56+
}
57+
};
58+
m_thread.emplace(wrapped_fn, args...);
59+
}
60+
61+
void check_await() {
62+
if (!m_thread.has_value()) throw std::logic_error("No thread started");
63+
}
64+
65+
bool has_payload() {
66+
if (!m_thread.has_value()) throw std::logic_error("No thread started");
67+
std::lock_guard l_(m_mutex);
68+
return m_eptr || m_payload.has_value();
69+
}
70+
71+
auto payload() {
72+
if (!m_thread.has_value()) throw std::logic_error("No thread started");
73+
m_thread->join();
74+
m_thread.reset();
75+
if (m_eptr) std::rethrow_exception(m_eptr);
76+
return *m_payload;
77+
}
78+
79+
void await() {
80+
if (!m_thread.has_value()) throw std::logic_error("No thread started");
81+
std::lock_guard l_(m_mutex);
82+
if (m_eptr || m_payload.has_value()) {
83+
resolve();
84+
}
85+
}
86+
87+
private:
88+
std::optional<std::jthread> m_thread;
89+
std::optional<Result> m_payload;
90+
std::recursive_mutex m_mutex;
91+
std::exception_ptr m_eptr;
4292
};
43-
private:
44-
std::function<Result(Args...)> m_fn;
45-
std::optional<std::jthread> m_thread;
46-
std::optional<Result> m_payload;
47-
std::recursive_mutex m_mutex;
48-
awaitable * m_awaitable = nullptr;
93+
template<>
94+
struct awaitable<void> {
95+
std::coroutine_handle<> awaiting = nullptr;
4996

50-
public:
97+
awaitable() = default;
98+
awaitable(awaitable && other) = delete;
99+
awaitable(awaitable const &) = delete;
100+
101+
bool await_ready() {
102+
return is_done();
103+
}
104+
105+
void await_suspend(std::coroutine_handle<> h) {
106+
// The awaiting coroutine is already suspended.
107+
awaiting = h;
108+
await();
109+
}
51110

52-
explicit co_thread(std::function<Result(Args...)> && fn) : m_fn(std::move(fn)) {}
111+
void await_resume() {
112+
done();
113+
}
53114

54-
co_thread & run(Args&&... args) {
55-
auto wrapped_fn = [this](Args... a) {
56-
auto result = m_fn(a...);
57-
{
58-
std::lock_guard l_(m_mutex);
59-
m_payload.emplace(result);
60-
if (m_awaitable) {
61-
m_awaitable->resolve();
115+
void resolve() {
116+
std::coroutine_handle<> a = nullptr;
117+
std::swap(a, awaiting);
118+
if (a) sigslot::resume_switch(a);
119+
}
120+
121+
template<typename Fn, typename ...Args>
122+
void run(Fn & fn, Args&&... args) {
123+
auto wrapped_fn = [this, &fn](Args... a) {
124+
try {
125+
fn(a...);
126+
{
127+
std::lock_guard l_(m_mutex);
128+
m_done = true;
129+
resolve();
130+
}
131+
} catch(...) {
132+
std::lock_guard l_(m_mutex);
133+
m_eptr = std::current_exception();
134+
resolve();
62135
}
63-
}
64-
};
65-
m_thread.emplace(wrapped_fn, args...);
66-
return *this;
67-
}
68-
auto & operator() (Args&&... args) {
69-
return this->run(args...);
70-
}
136+
};
137+
m_thread.emplace(wrapped_fn, args...);
138+
}
71139

72-
awaitable operator co_await() {
73-
if (!m_thread.has_value()) throw std::logic_error("No thread started");
74-
return awaitable(*this);
75-
}
140+
void check_await() {
141+
if (!m_thread.has_value()) throw std::logic_error("No thread started");
142+
}
76143

77-
bool has_payload() {
78-
if (!m_thread.has_value()) throw std::logic_error("No thread started");
79-
std::lock_guard l_(m_mutex);
80-
return m_payload.has_value();
81-
}
144+
bool is_done() {
145+
if (!m_thread.has_value()) throw std::logic_error("No thread started");
146+
std::lock_guard l_(m_mutex);
147+
return m_eptr || m_done;
148+
}
82149

83-
auto payload() {
84-
if (!m_thread.has_value()) throw std::logic_error("No thread started");
85-
m_thread->join();
86-
m_thread.reset();
87-
return *m_payload;
88-
}
150+
void done() {
151+
if (!m_thread.has_value()) throw std::logic_error("No thread started");
152+
m_thread->join();
153+
m_thread.reset();
154+
if (m_eptr) std::rethrow_exception(m_eptr);
155+
}
89156

90-
void await(awaitable * a) {
91-
if (!m_thread.has_value()) throw std::logic_error("No thread started");
92-
std::lock_guard l_(m_mutex);
93-
m_awaitable = a;
94-
if (m_payload.has_value()) {
95-
a->resolve();
157+
void await() {
158+
if (!m_thread.has_value()) throw std::logic_error("No thread started");
159+
std::lock_guard l_(m_mutex);
160+
if (m_eptr || m_done) {
161+
resolve();
162+
}
96163
}
164+
165+
private:
166+
std::optional<std::jthread> m_thread;
167+
bool m_done = false;
168+
std::exception_ptr m_eptr;
169+
std::recursive_mutex m_mutex;
170+
};
171+
template<typename T>
172+
struct awaitable_ptr {
173+
std::unique_ptr<awaitable<T>> m_guts;
174+
175+
awaitable_ptr() : m_guts(std::make_unique<awaitable<T>>()) {}
176+
awaitable_ptr(awaitable_ptr &&) = default;
177+
178+
awaitable<T> & operator co_await() {
179+
m_guts->check_await();
180+
return *m_guts;
181+
}
182+
};
183+
}
184+
185+
template<typename Callable>
186+
class co_thread {
187+
public:
188+
private:
189+
Callable m_fn;
190+
public:
191+
192+
template<typename ...Args>
193+
[[nodiscard]] auto operator() (Args && ...args) {
194+
cothread_internal::awaitable_ptr<decltype(m_fn(args...))> awaitable;
195+
awaitable.m_guts->run(m_fn, args...);
196+
return std::move(awaitable);
97197
}
198+
199+
explicit co_thread(Callable && fn) : m_fn(std::move(fn)) {}
98200
};
99201
}
100202

sigslot/sigslot.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,11 +236,19 @@ namespace sigslot {
236236

237237
// Helper for ptr-to-member; call the member function "normally".
238238
template<class desttype>
239+
requires std::derived_from<desttype, has_slots>
239240
void connect(desttype *pclass, void (desttype::* memfn)(args...), bool one_shot = false)
240241
{
241242
this->connect(pclass, [pclass, memfn](args... a) { (pclass->*memfn)(a...); }, one_shot);
242243
}
243244

245+
[[nodiscard]] std::unique_ptr<has_slots> connect(std::function<void(args...)> && fn, bool one_shot=false)
246+
{
247+
auto raii = std::make_unique<has_slots>();
248+
this->connect(raii.get(), std::move(fn), one_shot);
249+
return raii;
250+
}
251+
244252
// This code uses the long-hand because it assumes it may mutate the list.
245253
void emit(args... a)
246254
{

0 commit comments

Comments
 (0)