diff --git a/CMakeLists.txt b/CMakeLists.txt index 164f917..e8227e0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,17 +1,48 @@ cmake_minimum_required(VERSION 3.13) project(sigslot) -set (CMAKE_CXX_STANDARD 20) +# GoogleTest requires at least C++14, coroutines need C++20 +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) -include_directories(.) -add_executable(example example.cc sigslot/sigslot.h) +include(FetchContent) +FetchContent_Declare( + googletest + URL https://github.com/google/googletest/archive/03597a01ee50ed33e9dfd640b249b4be3799d395.zip +) +FetchContent_MakeAvailable(googletest) + +enable_testing() +link_libraries(GTest::gtest_main) +include_directories(${CMAKE_CURRENT_SOURCE_DIR}) +add_executable(sigslot-test + test/sigslot.cc + test/coroutine.cc + sigslot/sigslot.h + sigslot/tasklet.h + sigslot/resume.h +) +add_executable(sigslot-test-resume + sigslot/sigslot.h + sigslot/tasklet.h + test/resume.cc + sigslot/resume.h +) +add_executable(sigslot-test-cothread + sigslot/sigslot.h + sigslot/tasklet.h + test/cothread.cc + sigslot/resume.h + sigslot/cothread.h +) +include(GoogleTest) +gtest_discover_tests(sigslot-test) +gtest_discover_tests(sigslot-test-resume) +gtest_discover_tests(sigslot-test-cothread) -add_executable(co_example co_example.cc sigslot/sigslot.h) if (UNIX) - target_compile_options(co_example PUBLIC -fcoroutines) - target_link_options(co_example PUBLIC -fcoroutines) + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fcoroutines") endif () if (WIN32) - target_compile_options(co_example PUBLIC /await) + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /await") endif() -target_compile_definitions(co_example PUBLIC -DSIGSLOT_COROUTINES) diff --git a/README b/README deleted file mode 100644 index 3c7940e..0000000 --- a/README +++ /dev/null @@ -1,16 +0,0 @@ -sigslot - C++11 Signal/Slot library - -Originally written by Sarah Thompson. - -Various patches and fixes applied by Cat Nap Games: - -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. -I don't remember which ones though. - -C++11-erization by Dave Cridland: - -See test.cc for some documentation and a walk-through example. - -This is public domain; no copyright is claimed or asserted. - -No warranty is implied or offered either. diff --git a/README.md b/README.md new file mode 100644 index 0000000..fa0ff01 --- /dev/null +++ b/README.md @@ -0,0 +1,56 @@ +# sigslot - C++11 Signal/Slot library + +Originally written by Sarah Thompson. + +Various patches and fixes applied by Cat Nap Games: + +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. +I don't remember which ones though. + +C++11-erization (and C++2x-erixation, and mini coroutine library) by Dave Cridland: + +See example.cc and co_example.cc for some documentation and a walk-through example, or read the tests. + +This is public domain; no copyright is claimed or asserted. + +No warranty is implied or offered either. + +## Tagging and version + +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. + +## Promising, yet oddly vague and sometimes outright misleading documentation + +This library is a pure header library, and consists of four header files: + + + +This contains a sigslot::signal class, and a sigslot::has_slots class. + +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. + +Loosely, calling "emit(...)" on the signal will then call all the connected "slots", which are just arbitrary functions. + +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. + +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. + + + +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. + +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. + + + +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. + +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. + +If you don't, then std::coroutine_handle<>::resume() will be called directly (which works for trivial cases, but not for anything useful). + + + +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. + +This will not work with the built-in resumption, you'll need to implement *some* kind of event loop. diff --git a/sigslot/cothread.h b/sigslot/cothread.h new file mode 100644 index 0000000..7f8830f --- /dev/null +++ b/sigslot/cothread.h @@ -0,0 +1,203 @@ +// +// Created by dwd on 21/12/2021. +// + +#ifndef SIGSLOT_COTHREAD_H +#define SIGSLOT_COTHREAD_H + +#include +#include "sigslot/sigslot.h" +#include "sigslot/tasklet.h" + +namespace sigslot { + namespace cothread_internal { + template + struct awaitable { + std::coroutine_handle<> awaiting = nullptr; + + awaitable() = default; + awaitable(awaitable && other) = delete; + awaitable(awaitable const &) = delete; + + bool await_ready() { + return has_payload(); + } + + void await_suspend(std::coroutine_handle<> h) { + // The awaiting coroutine is already suspended. + awaiting = h; + await(); + } + + auto await_resume() { + return payload(); + } + + void resolve() { + std::coroutine_handle<> a = nullptr; + std::swap(a, awaiting); + if (a) sigslot::resume_switch(a); + } + + template + void run(Fn & fn, Args&&... args) { + auto wrapped_fn = [this, &fn](Args... a) { + try { + auto result = fn(a...); + { + std::lock_guard l_(m_mutex); + m_payload.emplace(result); + resolve(); + } + } catch(...) { + std::lock_guard l_(m_mutex); + m_eptr = std::current_exception(); + resolve(); + } + }; + m_thread.emplace(wrapped_fn, args...); + } + + void check_await() { + if (!m_thread.has_value()) throw std::logic_error("No thread started"); + } + + bool has_payload() { + if (!m_thread.has_value()) throw std::logic_error("No thread started"); + std::lock_guard l_(m_mutex); + return m_eptr || m_payload.has_value(); + } + + auto payload() { + if (!m_thread.has_value()) throw std::logic_error("No thread started"); + m_thread->join(); + m_thread.reset(); + if (m_eptr) std::rethrow_exception(m_eptr); + return *m_payload; + } + + void await() { + if (!m_thread.has_value()) throw std::logic_error("No thread started"); + std::lock_guard l_(m_mutex); + if (m_eptr || m_payload.has_value()) { + resolve(); + } + } + + private: + std::optional m_thread; + std::optional m_payload; + std::recursive_mutex m_mutex; + std::exception_ptr m_eptr; + }; + template<> + struct awaitable { + std::coroutine_handle<> awaiting = nullptr; + + awaitable() = default; + awaitable(awaitable && other) = delete; + awaitable(awaitable const &) = delete; + + bool await_ready() { + return is_done(); + } + + void await_suspend(std::coroutine_handle<> h) { + // The awaiting coroutine is already suspended. + awaiting = h; + await(); + } + + void await_resume() { + done(); + } + + void resolve() { + std::coroutine_handle<> a = nullptr; + std::swap(a, awaiting); + if (a) sigslot::resume_switch(a); + } + + template + void run(Fn & fn, Args&&... args) { + auto wrapped_fn = [this, &fn](Args... a) { + try { + fn(a...); + { + std::lock_guard l_(m_mutex); + m_done = true; + resolve(); + } + } catch(...) { + std::lock_guard l_(m_mutex); + m_eptr = std::current_exception(); + resolve(); + } + }; + m_thread.emplace(wrapped_fn, args...); + } + + void check_await() { + if (!m_thread.has_value()) throw std::logic_error("No thread started"); + } + + bool is_done() { + if (!m_thread.has_value()) throw std::logic_error("No thread started"); + std::lock_guard l_(m_mutex); + return m_eptr || m_done; + } + + void done() { + if (!m_thread.has_value()) throw std::logic_error("No thread started"); + m_thread->join(); + m_thread.reset(); + if (m_eptr) std::rethrow_exception(m_eptr); + } + + void await() { + if (!m_thread.has_value()) throw std::logic_error("No thread started"); + std::lock_guard l_(m_mutex); + if (m_eptr || m_done) { + resolve(); + } + } + + private: + std::optional m_thread; + bool m_done = false; + std::exception_ptr m_eptr; + std::recursive_mutex m_mutex; + }; + template + struct awaitable_ptr { + std::unique_ptr> m_guts; + + awaitable_ptr() : m_guts(std::make_unique>()) {} + awaitable_ptr(awaitable_ptr &&) = default; + + awaitable & operator co_await() { + m_guts->check_await(); + return *m_guts; + } + }; + } + + template + class co_thread { + public: + private: + Callable m_fn; + public: + + template + [[nodiscard]] auto operator() (Args && ...args) { + cothread_internal::awaitable_ptr awaitable; + awaitable.m_guts->run(m_fn, args...); + return std::move(awaitable); + } + + explicit co_thread(Callable && fn) : m_fn(std::move(fn)) {} + }; +} + +#endif diff --git a/sigslot/resume.h b/sigslot/resume.h new file mode 100644 index 0000000..4825fbf --- /dev/null +++ b/sigslot/resume.h @@ -0,0 +1,19 @@ +// +// Created by dave on 22/07/2024. +// + +#ifndef SIGSLOT_RESUME_H +#define SIGSLOT_RESUME_H + +#ifndef SIGSLOT_NO_COROUTINES +#include + +namespace sigslot { + namespace coroutines { + struct sentinel {}; + } + coroutines::sentinel resume(...); +} +#endif + +#endif //SIGSLOT_RESUME_H diff --git a/sigslot/sigslot.h b/sigslot/sigslot.h index 0564bf5..8e61375 100644 --- a/sigslot/sigslot.h +++ b/sigslot/sigslot.h @@ -11,8 +11,8 @@ // (see also the full documentation at http://sigslot.sourceforge.net/) // // #define switches -// SIGSLOT_COROUTINES: -// If defined, this will provide an operator co_await(), so that coroutines can +// SIGSLOT_NO_COROUTINES: +// If not defined, this will provide an operator co_await(), so that coroutines can // co_await on a signal instead of registering a callback. // // PLATFORM NOTES @@ -37,19 +37,28 @@ #include #include #include -#ifdef SIGSLOT_COROUTINES +#ifndef SIGSLOT_NO_COROUTINES #include #include #include #endif +#include + namespace sigslot { -#ifdef SIGSLOT_COROUTINES -#ifndef SIGSLOT_RESUME_OVERRIDE - inline void resume(std::coroutine_handle<> & coro) { +#ifndef SIGSLOT_NO_COROUTINES + template + inline void resume_dispatch(std::coroutine_handle<> coro) { + resume(coro); + } + template<> + inline void resume_dispatch(std::coroutine_handle<> coro) { coro.resume(); } -#endif + inline void resume_switch(std::coroutine_handle<> coro) { + using return_type = decltype(resume(coro)); + resume_dispatch(coro); + } #endif class has_slots; @@ -201,7 +210,7 @@ namespace sigslot { } -#ifdef SIGSLOT_COROUTINES +#ifndef SIGSLOT_NO_COROUTINES namespace coroutines { template struct awaitable; } @@ -227,15 +236,23 @@ namespace sigslot { // Helper for ptr-to-member; call the member function "normally". template + requires std::derived_from void connect(desttype *pclass, void (desttype::* memfn)(args...), bool one_shot = false) { this->connect(pclass, [pclass, memfn](args... a) { (pclass->*memfn)(a...); }, one_shot); } + [[nodiscard]] std::unique_ptr connect(std::function && fn, bool one_shot=false) + { + auto raii = std::make_unique(); + this->connect(raii.get(), std::move(fn), one_shot); + return raii; + } + // This code uses the long-hand because it assumes it may mutate the list. void emit(args... a) { -#ifdef SIGSLOT_COROUTINES +#ifndef SIGSLOT_NO_COROUTINES std::set *> awaitables(std::move(m_awaitables)); for (auto * awaitable : awaitables) { awaitable->resolve(a...); @@ -280,7 +297,7 @@ namespace sigslot { this->emit(a...); } -#ifdef SIGSLOT_COROUTINES +#ifndef SIGSLOT_NO_COROUTINES std::set *> m_awaitables; auto operator co_await() { @@ -298,7 +315,7 @@ namespace sigslot { }; -#ifdef SIGSLOT_COROUTINES +#ifndef SIGSLOT_NO_COROUTINES namespace coroutines { // Generic variant uses a tuple to pass back. template @@ -332,7 +349,7 @@ namespace sigslot { void resolve(args... a) { payload.emplace(a...); - if (awaiting) resume(awaiting); + if (awaiting) ::sigslot::resume_switch(awaiting); } ~awaitable() { @@ -371,7 +388,7 @@ namespace sigslot { void resolve(T a) { payload.emplace(a); - if (awaiting) resume(awaiting); + if (awaiting) ::sigslot::resume_switch(awaiting); } ~awaitable() { @@ -410,7 +427,7 @@ namespace sigslot { void resolve(T & a) { payload = &a; - if (awaiting) resume(awaiting); + if (awaiting) ::sigslot::resume_switch(awaiting); } ~awaitable() { @@ -447,7 +464,7 @@ namespace sigslot { void resolve() { ready = true; - if (awaiting) resume(awaiting); + if (awaiting) ::sigslot::resume_switch(awaiting); } ~awaitable() { diff --git a/sigslot/tasklet.h b/sigslot/tasklet.h index 969c24e..cd3605d 100644 --- a/sigslot/tasklet.h +++ b/sigslot/tasklet.h @@ -104,7 +104,7 @@ namespace sigslot { void resolve() { resolved = true; - if (awaiting) resume(awaiting); + if (awaiting) ::sigslot::resume_switch(awaiting); } virtual ~awaitable_base() = default; @@ -243,6 +243,7 @@ namespace sigslot { template struct tasklet : public internal::tasklet,T>>> { using promise_type = internal::promise_type,T>; + using value_type = T; }; template diff --git a/test/coroutine.cc b/test/coroutine.cc new file mode 100644 index 0000000..7c469e9 --- /dev/null +++ b/test/coroutine.cc @@ -0,0 +1,77 @@ +// +// Created by dave on 29/03/2024. +// + +#include +#include +#include + +namespace { + sigslot::tasklet trivial_task(int i) { + co_return i; + } + + sigslot::tasklet basic_task(sigslot::signal &signal) { + co_return co_await signal; + } + + sigslot::tasklet nested_task(int i) { + co_return co_await trivial_task(i); + } + + sigslot::tasklet exception_task(int i) { + if (i == 42) { + // Have to do this conditionally with a co_return otherwise it's not a coroutine. + throw std::runtime_error("Help"); + } + co_return i; + } +} + +TEST(Tasklet, Trivial) { + auto coro = trivial_task(42); + EXPECT_TRUE(coro.running()); + EXPECT_FALSE(coro.started()); + auto result = coro.get(); + EXPECT_FALSE(coro.running()); + EXPECT_TRUE(coro.started()); + EXPECT_EQ(result, 42); +} + +TEST(Tasklet, Basic) { + sigslot::signal signal; + + auto coro = basic_task(signal); + EXPECT_TRUE(coro.running()); + EXPECT_FALSE(coro.started()); + coro.start(); + EXPECT_TRUE(coro.running()); + EXPECT_TRUE(coro.started()); + signal(42); + auto result = coro.get(); + EXPECT_EQ(result, 42); +} + +TEST(Tasklet, Nested) { + auto coro = nested_task(42); + EXPECT_TRUE(coro.running()); + EXPECT_FALSE(coro.started()); + auto result = coro.get(); + EXPECT_FALSE(coro.running()); + EXPECT_TRUE(coro.started()); + EXPECT_EQ(result, 42); +} + + +TEST(Tasklet, Throw) { + auto coro = exception_task(42); + EXPECT_TRUE(coro.running()); + EXPECT_FALSE(coro.started()); + EXPECT_THROW( + auto result = coro.get(), + std::runtime_error + ); + EXPECT_FALSE(coro.running()); + EXPECT_TRUE(coro.started()); +} + diff --git a/test/cothread.cc b/test/cothread.cc new file mode 100644 index 0000000..8b2a3c2 --- /dev/null +++ b/test/cothread.cc @@ -0,0 +1,174 @@ +#include "gtest/gtest.h" +#include +#include +#include +#include +#include +// Tiny event loop. co_thread won't work properly without, +// since it's got to (essentially) block while the thread runs. + +std::mutex lock_me; +std::vector> resume_me; + + +namespace sigslot { + void resume(std::coroutine_handle<> coro) { + std::lock_guard l(lock_me); + resume_me.push_back(coro); + } +} + +#include +#include +#include + +template +void run_until_complete_low(sigslot::tasklet & coro) { + if (!coro.started()) coro.start(); + while (coro.running()) { + std::vector> current; + { + std::lock_guard l(lock_me); + current.swap(resume_me); + } + std::cout << "Resuming " << current.size() << " coroutines." << std::endl; + for (auto coro : current) { + coro.resume(); + } + current.clear(); + sleep(1); + std::cout << "... tick" << std::endl; + } +} +template +R run_until_complete(sigslot::tasklet & coro) { + run_until_complete_low(coro); + return coro.get(); +} +template<> +void run_until_complete(sigslot::tasklet & coro) { + run_until_complete_low(coro); + coro.get(); +} + + +sigslot::tasklet inner(std::string const & s) { + std::cout << "Here!" << std::endl; + sigslot::co_thread thread1([](std::string const &s) { + std::cout << "There 1! " << s << std::endl; + return true; + }); + sigslot::co_thread thread2([]() { + std::cout << "+ Launch" << std::endl; + sleep(1); + std::cout << "+ There 2!" << std::endl; + sleep(1); + std::cout << "+ End" << std::endl; + return true; + }); + std::cout << "Still here!" << std::endl; + auto thread2_await = thread2(); + auto result1 = co_await thread1(s); + std::cout << "Got result1:" << result1 << std::endl; + auto result2 = co_await thread2_await; + std::cout << "Got result2:" << result2 << std::endl; + co_return true; +} + +sigslot::tasklet start() { + std::string s = "Hello world!"; + auto result = co_await inner(s); + std::cout << "Completed test with result " << result << std::endl; +} + +namespace { + sigslot::tasklet trivial_task(int i) { + co_return i; + } + + sigslot::tasklet basic_task(sigslot::signal &signal) { + co_return co_await signal; + } + + sigslot::tasklet signal_thread_task() { + sigslot::signal signal; + sigslot::co_thread thread([&signal]() { + sleep(1); + signal(42); + sleep(1); + return 42; + }); + auto thread_result = thread(); + auto result = co_await signal; + co_await thread_result; + co_return result; + } + + sigslot::tasklet nested_task(int i) { + co_return co_await trivial_task(i); + } + + sigslot::tasklet exception_task(int i) { + if (i == 42) { + // Have to do this conditionally with a co_return otherwise it's not a coroutine. + throw std::runtime_error("Help"); + } + co_return i; + } + + sigslot::tasklet thread_exception_task() { + sigslot::co_thread t([]{throw std::runtime_error("Potato!");}); + co_await t(); + } +} + +TEST(CoThreadTest, CheckLoop) { + auto coro = trivial_task(42); + auto result = run_until_complete(coro); + EXPECT_EQ(result, 42); +} + +TEST(CoThreadTest, CheckLoop2) { + sigslot::signal signal; + auto coro = basic_task(signal); + coro.start(); + int i = 0; + while (coro.running()) { + std::vector> current; + { + std::lock_guard l(lock_me); + current.swap(resume_me); + } + std::cout << "Resuming " << current.size() << " coroutines." << std::endl; + for (auto coro : current) { + coro.resume(); + } + current.clear(); + sleep(1); + if (i == 2) { + std::cout << "Signalling" << std::endl; + signal(42); + } + ++i; + std::cout << "... tick" << std::endl; + } + auto result = coro.get(); + std::cout << "Result: " << result << std::endl; +} + +TEST(CoThreadTest, Tests) { + std::cout << "Start" << std::endl; + auto coro = start(); + run_until_complete(coro); + std::cout << "*** END ***" << std::endl; +} + +TEST(CoThreadTest, Exception) { + std::cout << "Start" << std::endl; + auto coro = thread_exception_task(); + EXPECT_THROW( + run_until_complete(coro), + std::runtime_error + ); + std::cout << "*** END ***" << std::endl; +} diff --git a/test/resume.cc b/test/resume.cc new file mode 100644 index 0000000..1774f9c --- /dev/null +++ b/test/resume.cc @@ -0,0 +1,58 @@ +// +// Created by dave on 29/03/2024. +// + +#include + +int resumptions = 0; + +namespace sigslot { + static inline void resume(std::coroutine_handle<> coro) { + ++resumptions; + coro.resume(); + } +} +#include +#include +#include + + +namespace { + sigslot::tasklet trivial_task(int i) { + co_return i; + } + + sigslot::tasklet basic_task(sigslot::signal &signal) { + co_return co_await signal; + } +} + +TEST(Resume, Trivial) { + EXPECT_EQ(resumptions, 0); + auto coro = trivial_task(42); + EXPECT_TRUE(coro.running()); + EXPECT_FALSE(coro.started()); + auto result = coro.get(); + EXPECT_FALSE(coro.running()); + EXPECT_TRUE(coro.started()); + EXPECT_EQ(result, 42); + EXPECT_EQ(resumptions, 0); + resumptions = 0; +} + +TEST(Resume, Basic) { + EXPECT_EQ(resumptions, 0); + sigslot::signal signal; + + auto coro = basic_task(signal); + EXPECT_TRUE(coro.running()); + EXPECT_FALSE(coro.started()); + coro.start(); + EXPECT_TRUE(coro.running()); + EXPECT_TRUE(coro.started()); + signal(42); + auto result = coro.get(); + EXPECT_EQ(result, 42); + EXPECT_EQ(resumptions, 1); + resumptions = 0; +} diff --git a/test/sigslot.cc b/test/sigslot.cc new file mode 100644 index 0000000..799e482 --- /dev/null +++ b/test/sigslot.cc @@ -0,0 +1,120 @@ +// +// Created by dave on 28/03/2024. +// + +#include +#include + +template +class Sink : public sigslot::has_slots { +public: + std::optional> result; + void slot(Args... args) { + result.emplace(args...); + } + void reset() { + result.reset(); + } +}; + +template<> +class Sink : public sigslot::has_slots { +public: + bool result = false; + void slot() { + result = true; + } + void reset() { + result = false; + } +}; + +TEST(Simple, test_bool) { + Sink sink; + EXPECT_FALSE(sink.result.has_value()); + sigslot::signal signal; + signal.connect(&sink, &Sink::slot); + signal(true); + EXPECT_TRUE(sink.result.has_value()); + EXPECT_TRUE(std::get<0>(*sink.result)); + sink.reset(); + signal(false); + EXPECT_TRUE(sink.result.has_value()); + EXPECT_FALSE(std::get<0>(*sink.result)); +} + + +TEST(Simple, test_bool_disconnect) { + sigslot::signal signal; + signal(true); + { + Sink sink; + EXPECT_FALSE(sink.result.has_value()); + sigslot::signal signal; + signal.connect(&sink, &Sink::slot); + signal(true); + EXPECT_TRUE(sink.result.has_value()); + EXPECT_TRUE(std::get<0>(*sink.result)); + } + signal(false); +} + + +TEST(Simple, test_bool_oneshot) { + Sink sink; + EXPECT_FALSE(sink.result.has_value()); + sigslot::signal signal; + signal.connect(&sink, &Sink::slot, true); + signal(true); + EXPECT_TRUE(sink.result.has_value()); + EXPECT_TRUE(std::get<0>(*sink.result)); + sink.reset(); + signal(false); + EXPECT_FALSE(sink.result.has_value()); +} + + +TEST(Simple, test_void) { + Sink sink; + EXPECT_FALSE(sink.result); + sigslot::signal<> signal; + signal.connect(&sink, &Sink::slot); + signal(); + EXPECT_TRUE(sink.result); + sink.reset(); + signal(); + EXPECT_TRUE(sink.result); +} + + +TEST(Simple, test_void_oneshot) { + Sink sink; + EXPECT_FALSE(sink.result); + sigslot::signal<> signal; + signal.connect(&sink, &Sink::slot, true); + signal(); + EXPECT_TRUE(sink.result); + sink.reset(); + signal(); + EXPECT_FALSE(sink.result); +} + +TEST(Simple, test_raii_slot) { + Sink sink; + EXPECT_FALSE(sink.result); + sigslot::signal<> signal; + { + auto scope_slot = signal.connect([&sink]() { + sink.slot(); + }); + signal(); + EXPECT_TRUE(sink.result); + sink.reset(); + EXPECT_FALSE(sink.result); + signal(); + EXPECT_TRUE(sink.result); + } + sink.reset(); + signal(); + EXPECT_FALSE(sink.result); +}