diff --git a/docs/multi-tasking/future.md b/docs/multi-tasking/future.md new file mode 100644 index 00000000..8e131d45 --- /dev/null +++ b/docs/multi-tasking/future.md @@ -0,0 +1,247 @@ +--- +title: "future / promise" +--- + +{{< callout type="info">}} + Header: `future.h` + From: `20.50.0` +{{< /callout >}} + +A lightweight, heap-free `future` / `promise` pair for passing a single result from a producer (e.g. an ISR, a +thread or an RPC response handler) to a consumer. + +Unlike `std::future`/`std::promise`, the shared state is **not** allocated. It is an `etl::shared_state` object +whose storage and lifetime are managed by the user, for example as an element of a fixed-size pending-request table. +`etl::future` and `etl::promise` are non-owning handles to it. + +```cpp +template +class shared_state; + +template +class future; + +template +class promise; +``` + +All three are specialised for `void`, where there is no value, only a *ready* notification, and for references +(`T&`), where a pointer to the referenced object is stored. `T` must not be an array type. + +**Differences from the standard library** +- `etl::future` has the semantics of `std::shared_future`: it is copyable, and `get()` does not consume the + value, so it may be called more than once, by every copy. `get()` returns `const T&` (`R&` for `future`). +- `etl::promise` is, like `std::promise`, move-only (C++11) or non-copyable (C++03). +- `get()` does not block. Poll `is_ready()` or use an external notification. +- There is no `set_exception()` and no *broken promise* notification. A consumer cannot distinguish a slow producer + from one that has gone away. + +**Availability** +The header requires `etl::atomic_bool`. The classes are only defined when `ETL_HAS_ATOMIC` is `1` +(see `etl::atomic`). + +**Thread safety** +`set_value()` and `is_ready()`/`get()` may be called from different threads or contexts, provided that at most +**one** context calls `set_value()` and at most **one** context calls `get()`. The ready flag uses +release/acquire ordering, so a value written by `set_value()` is visible after `is_ready()` returns `true`. +`reset()` must not be called concurrently with any other member. + +## shared_state + +Holds the result and an atomic *ready* flag. Non-copyable. + +```cpp +shared_state() +``` +**Description** +Constructs a state that is not ready. + +--- + +```cpp +~shared_state() +``` +**Description** +Destroys the stored value, if one has been set. + +--- + +```cpp +void set_value(const T& value) +void set_value(T&& value) // C++11 +void set_value(T& value) // shared_state only +void set_value() // shared_state only +``` +**Description** +Stores the value (or, for `shared_state`, the address of the referenced object) and marks the state as ready. +Asserts `etl::promise_already_satisfied` if the state is already ready. Call `reset()` first to reuse the state. + +--- + +```cpp +bool is_ready() const noexcept +``` +**Description** +Returns `true` if a value has been set. + +--- + +```cpp +T& get() +const T& get() const +T& get() const // shared_state only +``` +**Description** +Returns a reference to the stored value. +Not available for `shared_state`. +Asserts `etl::future_not_ready` if the state is not ready. + +--- + +```cpp +void reset() +``` +**Description** +Destroys the stored value, if any, and marks the state as not ready, so that it can be reused. + +## future + +A non-owning handle to a `shared_state`. Copyable, with the semantics of `std::shared_future`. + +```cpp +future() noexcept +``` +**Description** +Constructs a future that is not associated with a shared state. `valid()` returns `false`. + +--- + +```cpp +explicit future(etl::shared_state& state) noexcept +``` +**Description** +Constructs a future associated with `state`. + +--- + +```cpp +bool valid() const noexcept +``` +**Description** +Returns `true` if the future is associated with a shared state. + +--- + +```cpp +bool is_ready() const noexcept +``` +**Description** +Returns `true` if the future is valid and the result is available. + +--- + +```cpp +const T& get() const +R& get() const // future only +void get() const // future only +``` +**Description** +Returns the result. +Asserts `etl::future_no_state` if the future is not valid (default constructed, no shared state). +Asserts `etl::future_not_ready` if the future is valid but the result is not yet available. +Does not block. Poll `is_ready()` or use an external notification first. +The value is not moved out and the future remains valid, so `get()` may be called more than once. + +## promise + +A non-owning handle to a `shared_state` that fulfils it. Move-only (C++11) or non-copyable (C++03), so that there is +only one producer. + +```cpp +promise() noexcept +``` +**Description** +Constructs a promise that is not associated with a shared state. `valid()` returns `false`. + +--- + +```cpp +explicit promise(etl::shared_state& state) noexcept +``` +**Description** +Constructs a promise associated with `state`. + +--- + +```cpp +promise(promise&& other) noexcept // C++11 +promise& operator=(promise&& other) noexcept // C++11 +``` +**Description** +Transfers the association with the shared state. `other` is no longer valid. + +--- + +```cpp +bool valid() const noexcept +``` +**Description** +Returns `true` if the promise is associated with a shared state. + +--- + +```cpp +etl::future get_future() +``` +**Description** +Returns a future associated with the same shared state. +May be called more than once. All futures refer to the same state. +Asserts `etl::future_no_state` if the promise is not valid. + +--- + +```cpp +void set_value(const T& value) +void set_value(T&& value) // C++11 +void set_value(R& value) // promise only +void set_value() // promise only +``` +**Description** +Fulfils the promise. +Asserts `etl::future_no_state` if the promise is not valid. +Asserts `etl::promise_already_satisfied` if the shared state is already ready. + +## Errors + +| Exception | Reason | Error text | +| -------------------------------- | -------------------------------------------------------- | --------------------------- | +| `etl::future_exception` | Base of all future/promise exceptions. | | +| `etl::future_not_ready` | `get()` on a future or state that is not ready. | `future:not ready` | +| `etl::promise_already_satisfied` | `set_value()` on a state that is already ready. | `promise:already satisfied` | +| `etl::future_no_state` | Use of a `future` or `promise` that has no shared state. | `future:no state` | + +`etl::future_exception` is derived from `etl::exception`. + +## Example + +```cpp +#include "etl/future.h" + +etl::shared_state state; // Owned by the application. + +// Consumer +etl::promise promise(state); +etl::future future = promise.get_future(); + +// Producer (e.g. in an ISR or another thread) +promise.set_value(42); + +// Consumer +if (future.is_ready()) +{ + int result = future.get(); // 42 +} + +// Reuse the state for the next request. +state.reset(); +``` diff --git a/include/etl/file_error_numbers.h b/include/etl/file_error_numbers.h index 1bcddc48..76175a77 100644 --- a/include/etl/file_error_numbers.h +++ b/include/etl/file_error_numbers.h @@ -112,5 +112,6 @@ SOFTWARE. #define ETL_FORMAT_FILE_ID "79" #define ETL_INPLACE_FUNCTION_FILE_ID "80" #define ETL_INTRUSIVE_AVL_TREE_FILE_ID "81" -#define ETL_CYCLIC_VALUE_FILE_ID "82" +#define ETL_FUTURE_FILE_ID "82" +#define ETL_CYCLIC_VALUE_FILE_ID "83" #endif diff --git a/include/etl/future.h b/include/etl/future.h new file mode 100644 index 00000000..da401285 --- /dev/null +++ b/include/etl/future.h @@ -0,0 +1,603 @@ +///\file + +/****************************************************************************** +The MIT License(MIT) + +Embedded Template Library. +https://github.com/ETLCPP/etl +https://www.etlcpp.com + +Copyright(c) 2026 BMW AG + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files(the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions : + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +******************************************************************************/ + +#ifndef ETL_FUTURE_INCLUDED +#define ETL_FUTURE_INCLUDED + +#include "platform.h" +#include "alignment.h" +#include "atomic.h" +#include "error_handler.h" +#include "exception.h" +#include "file_error_numbers.h" +#include "memory.h" +#include "nullptr.h" +#include "placement_new.h" +#include "static_assert.h" +#include "type_traits.h" +#include "utility.h" + +#if ETL_HAS_ATOMIC + +namespace etl +{ + //*************************************************************************** + /// Exception types for future/promise. + //*************************************************************************** + class future_exception : public etl::exception + { + public: + + future_exception(string_type reason_, string_type file_name_, numeric_type line_number_) + : exception(reason_, file_name_, line_number_) + { + } + }; + + class future_not_ready : public etl::future_exception + { + public: + + future_not_ready(string_type file_name_, numeric_type line_number_) + : future_exception(ETL_ERROR_TEXT("future:not ready", ETL_FUTURE_FILE_ID"A"), file_name_, line_number_) + { + } + }; + + class promise_already_satisfied : public etl::future_exception + { + public: + + promise_already_satisfied(string_type file_name_, numeric_type line_number_) + : future_exception(ETL_ERROR_TEXT("promise:already satisfied", ETL_FUTURE_FILE_ID"B"), file_name_, line_number_) + { + } + }; + + class future_no_state : public etl::future_exception + { + public: + + future_no_state(string_type file_name_, numeric_type line_number_) + : future_exception(ETL_ERROR_TEXT("future:no state", ETL_FUTURE_FILE_ID"C"), file_name_, line_number_) + { + } + }; + + //*************************************************************************** + /// Shared state between a promise and a future. + /// No heap allocation. The storage and lifetime of the shared_state are + /// managed by the user, e.g. as an element of a fixed-size table. + /// + /// Thread safety: set_value() and is_ready()/get() may be called from + /// different threads provided that at most ONE thread calls set_value() + /// and at most ONE thread calls get(). The atomic ready flag provides + /// acquire/release ordering. + //*************************************************************************** + template + class shared_state + { + public: + + ETL_STATIC_ASSERT(!etl::is_array::value, "etl::shared_state: T must not be an array type"); + + shared_state() + : ready_(false) + { + } + + ~shared_state() + { + if (is_ready()) + { + ptr()->~T(); + } + } + + //************************************************************************* + /// Store the result value and mark the state as ready. + //************************************************************************* + void set_value(const T& value) + { + ETL_ASSERT(!is_ready(), ETL_ERROR(promise_already_satisfied)); + ::new (static_cast(&storage_)) T(value); + ready_.store(true, etl::memory_order_release); + } + + #if ETL_USING_CPP11 + //************************************************************************* + /// Store the result value (move) and mark the state as ready. + //************************************************************************* + void set_value(T&& value) + { + ETL_ASSERT(!is_ready(), ETL_ERROR(promise_already_satisfied)); + ::new (static_cast(&storage_)) T(etl::move(value)); + ready_.store(true, etl::memory_order_release); + } + #endif + + //************************************************************************* + /// Check whether the result is available. + //************************************************************************* + ETL_NODISCARD + bool is_ready() const ETL_NOEXCEPT + { + return ready_.load(etl::memory_order_acquire); + } + + //************************************************************************* + /// Get the stored value. + /// Asserts etl::future_not_ready if the state is not ready. + //************************************************************************* + ETL_NODISCARD + T& get() + { + ETL_ASSERT(is_ready(), ETL_ERROR(future_not_ready)); + return *ptr(); + } + + ETL_NODISCARD + const T& get() const + { + ETL_ASSERT(is_ready(), ETL_ERROR(future_not_ready)); + return *ptr(); + } + + //************************************************************************* + /// Reset for reuse. + //************************************************************************* + void reset() + { + if (is_ready()) + { + ptr()->~T(); + ready_.store(false, etl::memory_order_release); + } + } + + private: + + //************************************************************************* + /// Pointer to the object in the storage. + /// Laundered, as the storage is reused after reset(). + //************************************************************************* + T* ptr() + { + return etl::launder(reinterpret_cast(&storage_)); + } + + const T* ptr() const + { + return etl::launder(reinterpret_cast(&storage_)); + } + + // Non-copyable. + shared_state(const shared_state&) ETL_DELETE; + shared_state& operator=(const shared_state&) ETL_DELETE; + + typename etl::aligned_storage::value>::type storage_; + etl::atomic_bool ready_; + }; + + //*************************************************************************** + /// Specialisation for references — stores a pointer to the referenced object. + //*************************************************************************** + template + class shared_state + { + public: + + shared_state() + : p_value_(ETL_NULLPTR) + , ready_(false) + { + } + + //************************************************************************* + /// Store the reference and mark the state as ready. + //************************************************************************* + void set_value(T& value) + { + ETL_ASSERT(!is_ready(), ETL_ERROR(promise_already_satisfied)); + p_value_ = etl::addressof(value); + ready_.store(true, etl::memory_order_release); + } + + //************************************************************************* + /// Check whether the result is available. + //************************************************************************* + ETL_NODISCARD + bool is_ready() const ETL_NOEXCEPT + { + return ready_.load(etl::memory_order_acquire); + } + + //************************************************************************* + /// Get the stored reference. + /// Asserts etl::future_not_ready if the state is not ready. + //************************************************************************* + ETL_NODISCARD + T& get() const + { + ETL_ASSERT(is_ready(), ETL_ERROR(future_not_ready)); + return *p_value_; + } + + //************************************************************************* + /// Reset for reuse. + //************************************************************************* + void reset() + { + ready_.store(false, etl::memory_order_release); + p_value_ = ETL_NULLPTR; + } + + private: + + // Non-copyable. + shared_state(const shared_state&) ETL_DELETE; + shared_state& operator=(const shared_state&) ETL_DELETE; + + T* p_value_; + etl::atomic_bool ready_; + }; + + //*************************************************************************** + /// Specialisation for void — no value storage, just a ready flag. + //*************************************************************************** + template <> + class shared_state + { + public: + + shared_state() + : ready_(false) + { + } + + void set_value() + { + ETL_ASSERT(!is_ready(), ETL_ERROR(promise_already_satisfied)); + ready_.store(true, etl::memory_order_release); + } + + ETL_NODISCARD + bool is_ready() const ETL_NOEXCEPT + { + return ready_.load(etl::memory_order_acquire); + } + + void reset() + { + ready_.store(false, etl::memory_order_release); + } + + private: + + shared_state(const shared_state&) ETL_DELETE; + shared_state& operator=(const shared_state&) ETL_DELETE; + + etl::atomic_bool ready_; + }; + + namespace private_future + { + //************************************************************************* + /// The result type of future::get(). + /// const T& for values, R& for T = R&. + /// (C++03 has no reference collapsing, so const T& cannot be used for both.) + //************************************************************************* + template + struct future_get_result + { + typedef const T& type; + }; + + template + struct future_get_result + { + typedef T& type; + }; + } // namespace private_future + + //*************************************************************************** + /// A lightweight future — a non-owning handle to a shared_state. + /// The shared_state lifetime must be managed externally. + /// Has the semantics of std::shared_future: it is copyable, get() does not + /// consume the value and may be called more than once, by every copy. + /// get() returns const T&, or R& for T = R&. + //*************************************************************************** + template + class future + { + public: + + future() ETL_NOEXCEPT + : state_(ETL_NULLPTR) + { + } + + explicit future(shared_state& state) ETL_NOEXCEPT + : state_(&state) + { + } + + //************************************************************************* + /// Returns true when the result is available. + //************************************************************************* + ETL_NODISCARD + bool is_ready() const ETL_NOEXCEPT + { + return (state_ != ETL_NULLPTR) && state_->is_ready(); + } + + //************************************************************************* + /// Returns true if this future is associated with a shared state. + //************************************************************************* + ETL_NODISCARD + bool valid() const ETL_NOEXCEPT + { + return state_ != ETL_NULLPTR; + } + + //************************************************************************* + /// Get the result. + /// Asserts etl::future_no_state if not valid (no shared state). + /// Asserts etl::future_not_ready if valid but the result is not ready. + //************************************************************************* + ETL_NODISCARD + typename private_future::future_get_result::type get() const + { + ETL_ASSERT(valid(), ETL_ERROR(future_no_state)); + ETL_ASSERT(!valid() || state_->is_ready(), ETL_ERROR(future_not_ready)); + return state_->get(); + } + + private: + + shared_state* state_; + }; + + //*************************************************************************** + /// Specialisation for void. + //*************************************************************************** + template <> + class future + { + public: + + future() ETL_NOEXCEPT + : state_(ETL_NULLPTR) + { + } + + explicit future(shared_state& state) ETL_NOEXCEPT + : state_(&state) + { + } + + ETL_NODISCARD + bool is_ready() const ETL_NOEXCEPT + { + return (state_ != ETL_NULLPTR) && state_->is_ready(); + } + + ETL_NODISCARD + bool valid() const ETL_NOEXCEPT + { + return state_ != ETL_NULLPTR; + } + + //************************************************************************* + /// Asserts etl::future_no_state if not valid (no shared state). + /// Asserts etl::future_not_ready if valid but the result is not ready. + //************************************************************************* + void get() const + { + ETL_ASSERT(valid(), ETL_ERROR(future_no_state)); + ETL_ASSERT(!valid() || state_->is_ready(), ETL_ERROR(future_not_ready)); + } + + private: + + shared_state* state_; + }; + + namespace private_future + { + //************************************************************************* + /// Common base of the promise specialisations. + /// Like std::promise, a promise is move-only (C++11), so there is only + /// ever one producer. Non-copyable in C++03. + //************************************************************************* + template + class promise_base + { + public: + + //*********************************************************************** + /// Returns true if the promise is associated with a shared state. + //*********************************************************************** + ETL_NODISCARD + bool valid() const ETL_NOEXCEPT + { + return state_ != ETL_NULLPTR; + } + + //*********************************************************************** + /// Get the associated future. + /// Asserts etl::future_no_state if the promise is not valid. + //*********************************************************************** + ETL_NODISCARD + etl::future get_future() + { + ETL_ASSERT(valid(), ETL_ERROR(future_no_state)); + return etl::future(*state_); + } + + protected: + + promise_base() ETL_NOEXCEPT + : state_(ETL_NULLPTR) + { + } + + explicit promise_base(etl::shared_state& state) ETL_NOEXCEPT + : state_(&state) + { + } + + #if ETL_USING_CPP11 + //*********************************************************************** + /// Move constructor. The moved from promise is no longer valid. + //*********************************************************************** + promise_base(promise_base&& other) ETL_NOEXCEPT + : state_(other.state_) + { + other.state_ = ETL_NULLPTR; + } + + //*********************************************************************** + /// Move assignment. The moved from promise is no longer valid. + //*********************************************************************** + promise_base& operator=(promise_base&& other) ETL_NOEXCEPT + { + if (this != &other) + { + state_ = other.state_; + other.state_ = ETL_NULLPTR; + } + + return *this; + } + #endif + + etl::shared_state* state_; + + private: + + // Non-copyable. + promise_base(const promise_base&) ETL_DELETE; + promise_base& operator=(const promise_base&) ETL_DELETE; + }; + } // namespace private_future + + //*************************************************************************** + /// A lightweight promise — a non-owning handle that can set the value + /// in a shared_state. + //*************************************************************************** + template + class promise : public private_future::promise_base + { + public: + + promise() ETL_NOEXCEPT + : private_future::promise_base() + { + } + + explicit promise(etl::shared_state& state) ETL_NOEXCEPT + : private_future::promise_base(state) + { + } + + //************************************************************************* + /// Set the value (fulfil the promise). + //************************************************************************* + void set_value(const T& value) + { + ETL_ASSERT(this->valid(), ETL_ERROR(future_no_state)); + this->state_->set_value(value); + } + + #if ETL_USING_CPP11 + void set_value(T&& value) + { + ETL_ASSERT(this->valid(), ETL_ERROR(future_no_state)); + this->state_->set_value(etl::move(value)); + } + #endif + }; + + //*************************************************************************** + /// Specialisation for references. + //*************************************************************************** + template + class promise : public private_future::promise_base + { + public: + + promise() ETL_NOEXCEPT + : private_future::promise_base() + { + } + + explicit promise(etl::shared_state& state) ETL_NOEXCEPT + : private_future::promise_base(state) + { + } + + void set_value(T& value) + { + ETL_ASSERT(this->valid(), ETL_ERROR(future_no_state)); + this->state_->set_value(value); + } + }; + + //*************************************************************************** + /// Specialisation for void. + //*************************************************************************** + template <> + class promise : public private_future::promise_base + { + public: + + promise() ETL_NOEXCEPT + : private_future::promise_base() + { + } + + explicit promise(etl::shared_state& state) ETL_NOEXCEPT + : private_future::promise_base(state) + { + } + + void set_value() + { + ETL_ASSERT(this->valid(), ETL_ERROR(future_no_state)); + this->state_->set_value(); + } + }; + +} // namespace etl + +#endif // ETL_HAS_ATOMIC + +#endif // ETL_FUTURE_INCLUDED diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 4f03d1ce..a03b5c5c 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -196,6 +196,7 @@ add_executable(etl_tests test_function.cpp test_functional.cpp test_function_traits.cpp + test_future.cpp test_gamma.cpp test_hash.cpp test_hfsm.cpp diff --git a/test/meson.build b/test/meson.build index b4bc601d..16890071 100644 --- a/test/meson.build +++ b/test/meson.build @@ -184,6 +184,7 @@ etl_test_sources = files( 'test_function.cpp', 'test_function_traits.cpp', 'test_functional.cpp', + 'test_future.cpp', 'test_gamma.cpp', 'test_hash.cpp', 'test_hfsm.cpp', diff --git a/test/syntax_check/CMakeLists.txt b/test/syntax_check/CMakeLists.txt index c50aa3de..a8964e31 100644 --- a/test/syntax_check/CMakeLists.txt +++ b/test/syntax_check/CMakeLists.txt @@ -234,6 +234,7 @@ target_sources(tests PRIVATE function.h.t.cpp functional.h.t.cpp function_traits.h.t.cpp + future.h.t.cpp gamma.h.t.cpp gcd.h.t.cpp generic_pool.h.t.cpp diff --git a/test/syntax_check/future.h.t.cpp b/test/syntax_check/future.h.t.cpp new file mode 100644 index 00000000..ab6dd473 --- /dev/null +++ b/test/syntax_check/future.h.t.cpp @@ -0,0 +1,29 @@ +/****************************************************************************** +The MIT License(MIT) + +Embedded Template Library. +https://github.com/ETLCPP/etl +https://www.etlcpp.com + +Copyright(c) 2026 BMW AG + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files(the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions : + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +******************************************************************************/ + +#include diff --git a/test/test_future.cpp b/test/test_future.cpp new file mode 100644 index 00000000..8875e1be --- /dev/null +++ b/test/test_future.cpp @@ -0,0 +1,628 @@ +/****************************************************************************** +The MIT License(MIT) + +Embedded Template Library. +https://github.com/ETLCPP/etl +https://www.etlcpp.com + +Copyright(c) 2026 BMW AG + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files(the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions : + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +******************************************************************************/ + +#include "unit_test_framework.h" + +#include "etl/future.h" + +#include + +#if ETL_HAS_ATOMIC + +namespace +{ + //--------------------------------------------------------------------------- + // A simple struct to test non-primitive types in future/promise. + //--------------------------------------------------------------------------- + struct Point + { + int x; + int y; + + bool operator==(const Point& other) const + { + return x == other.x && y == other.y; + } + }; + + //--------------------------------------------------------------------------- + // Counts the number of live objects, to check construction/destruction. + //--------------------------------------------------------------------------- + struct Counted + { + static int live; + + explicit Counted(int value_) + : value(value_) + { + ++live; + } + + Counted(const Counted& other) + : value(other.value) + { + ++live; + } + + ~Counted() + { + --live; + } + + int value; + }; + + int Counted::live = 0; + + //--------------------------------------------------------------------------- + // A move-only type that records whether it has been moved from. + //--------------------------------------------------------------------------- + struct MoveOnly + { + explicit MoveOnly(int value_) + : value(value_) + , moved_from(false) + { + } + + MoveOnly(MoveOnly&& other) + : value(other.value) + , moved_from(false) + { + other.moved_from = true; + } + + MoveOnly(const MoveOnly&) = delete; + MoveOnly& operator=(const MoveOnly&) = delete; + + int value; + bool moved_from; + }; + + SUITE(test_future) + { + //************************************************************************* + // shared_state tests + //************************************************************************* + + TEST(test_shared_state_int_default_not_ready) + { + etl::shared_state state; + CHECK(!state.is_ready()); + } + + TEST(test_shared_state_int_set_value_becomes_ready) + { + etl::shared_state state; + state.set_value(42); + CHECK(state.is_ready()); + CHECK_EQUAL(42, state.get()); + } + + TEST(test_shared_state_int_get_const) + { + etl::shared_state state; + state.set_value(77); + const etl::shared_state& cref = state; + CHECK_EQUAL(77, cref.get()); + } + + TEST(test_shared_state_int_reset) + { + etl::shared_state state; + state.set_value(10); + CHECK(state.is_ready()); + state.reset(); + CHECK(!state.is_ready()); + } + + TEST(test_shared_state_int_reset_when_not_ready) + { + etl::shared_state state; + // Should be safe to reset even when not ready. + state.reset(); + CHECK(!state.is_ready()); + } + + TEST(test_shared_state_double) + { + etl::shared_state state; + state.set_value(3.14); + CHECK(state.is_ready()); + CHECK_CLOSE(3.14, state.get(), 0.001); + } + + TEST(test_shared_state_struct) + { + etl::shared_state state; + Point p{10, 20}; + state.set_value(p); + CHECK(state.is_ready()); + CHECK_EQUAL(10, state.get().x); + CHECK_EQUAL(20, state.get().y); + } + + //************************************************************************* + // shared_state tests + //************************************************************************* + + TEST(test_shared_state_void_default_not_ready) + { + etl::shared_state state; + CHECK(!state.is_ready()); + } + + TEST(test_shared_state_void_set_value_becomes_ready) + { + etl::shared_state state; + state.set_value(); + CHECK(state.is_ready()); + } + + TEST(test_shared_state_void_reset) + { + etl::shared_state state; + state.set_value(); + CHECK(state.is_ready()); + state.reset(); + CHECK(!state.is_ready()); + } + + //************************************************************************* + // future tests + //************************************************************************* + + TEST(test_future_int_default_invalid) + { + etl::future f; + CHECK(!f.valid()); + CHECK(!f.is_ready()); + } + + TEST(test_future_int_from_state_valid_not_ready) + { + etl::shared_state state; + etl::future f(state); + CHECK(f.valid()); + CHECK(!f.is_ready()); + } + + TEST(test_future_int_becomes_ready_after_set) + { + etl::shared_state state; + etl::future f(state); + state.set_value(55); + CHECK(f.is_ready()); + CHECK_EQUAL(55, f.get()); + } + + TEST(test_future_int_const_get) + { + etl::shared_state state; + state.set_value(88); + const etl::future f(state); + CHECK(f.is_ready()); + CHECK_EQUAL(88, f.get()); + } + + TEST(test_future_double) + { + etl::shared_state state; + etl::future f(state); + state.set_value(2.718); + CHECK(f.is_ready()); + CHECK_CLOSE(2.718, f.get(), 0.001); + } + + TEST(test_future_struct) + { + etl::shared_state state; + etl::future f(state); + state.set_value(Point{3, 4}); + CHECK(f.is_ready()); + CHECK_EQUAL(3, f.get().x); + CHECK_EQUAL(4, f.get().y); + } + + //************************************************************************* + // future tests + //************************************************************************* + + TEST(test_future_void_default_invalid) + { + etl::future f; + CHECK(!f.valid()); + CHECK(!f.is_ready()); + } + + TEST(test_future_void_from_state) + { + etl::shared_state state; + etl::future f(state); + CHECK(f.valid()); + CHECK(!f.is_ready()); + state.set_value(); + CHECK(f.is_ready()); + } + + //************************************************************************* + // promise tests + //************************************************************************* + + TEST(test_promise_int_default_invalid) + { + etl::promise p; + CHECK(!p.valid()); + } + + TEST(test_promise_int_set_value_fulfils_future) + { + etl::shared_state state; + etl::promise p(state); + etl::future f = p.get_future(); + CHECK(f.valid()); + CHECK(!f.is_ready()); + p.set_value(123); + CHECK(f.is_ready()); + CHECK_EQUAL(123, f.get()); + } + + TEST(test_promise_double) + { + etl::shared_state state; + etl::promise p(state); + etl::future f = p.get_future(); + p.set_value(1.41421); + CHECK(f.is_ready()); + CHECK_CLOSE(1.41421, f.get(), 0.00001); + } + + TEST(test_promise_struct) + { + etl::shared_state state; + etl::promise p(state); + etl::future f = p.get_future(); + p.set_value(Point{-5, 7}); + CHECK(f.is_ready()); + CHECK_EQUAL(-5, f.get().x); + CHECK_EQUAL(7, f.get().y); + } + + //************************************************************************* + // promise tests + //************************************************************************* + + TEST(test_promise_void_default_invalid) + { + etl::promise p; + CHECK(!p.valid()); + } + + TEST(test_promise_void_set_value_fulfils_future) + { + etl::shared_state state; + etl::promise p(state); + etl::future f = p.get_future(); + CHECK(f.valid()); + CHECK(!f.is_ready()); + p.set_value(); + CHECK(f.is_ready()); + } + + //************************************************************************* + // Integration / edge-case tests + //************************************************************************* + + TEST(test_multiple_futures_same_state) + { + // Two futures can observe the same shared_state. + etl::shared_state state; + etl::future f1(state); + etl::future f2(state); + CHECK(!f1.is_ready()); + CHECK(!f2.is_ready()); + state.set_value(999); + CHECK(f1.is_ready()); + CHECK(f2.is_ready()); + CHECK_EQUAL(999, f1.get()); + CHECK_EQUAL(999, f2.get()); + } + + TEST(test_shared_state_reuse_after_reset) + { + etl::shared_state state; + etl::promise p(state); + etl::future f = p.get_future(); + p.set_value(1); + CHECK_EQUAL(1, f.get()); + + // Reset and reuse. + state.reset(); + CHECK(!f.is_ready()); + + // Set a different value. + state.set_value(2); + CHECK(f.is_ready()); + CHECK_EQUAL(2, f.get()); + } + + TEST(test_move_semantics_set_value) + { + etl::shared_state state; + etl::promise p(state); + etl::future f = p.get_future(); + MoveOnly value(50); + p.set_value(etl::move(value)); + CHECK(value.moved_from); + CHECK(f.is_ready()); + CHECK_EQUAL(50, f.get().value); + CHECK(!f.get().moved_from); + } + + //************************************************************************* + // Lifetime of the stored value + //************************************************************************* + + TEST(test_shared_state_destroys_value) + { + Counted::live = 0; + { + etl::shared_state state; + CHECK_EQUAL(0, Counted::live); + state.set_value(Counted(1)); + CHECK_EQUAL(1, Counted::live); + } + CHECK_EQUAL(0, Counted::live); + } + + TEST(test_shared_state_not_ready_destroys_nothing) + { + Counted::live = 0; + { + etl::shared_state state; + } + CHECK_EQUAL(0, Counted::live); + } + + TEST(test_shared_state_reset_destroys_value_and_reuse) + { + Counted::live = 0; + { + etl::shared_state state; + state.set_value(Counted(1)); + CHECK_EQUAL(1, Counted::live); + state.reset(); + CHECK_EQUAL(0, Counted::live); + CHECK(!state.is_ready()); + state.set_value(Counted(2)); + CHECK_EQUAL(1, Counted::live); + CHECK_EQUAL(2, state.get().value); + } + CHECK_EQUAL(0, Counted::live); + } + + //************************************************************************* + // Reference specialisations + //************************************************************************* + + TEST(test_reference) + { + int value = 1; + etl::shared_state state; + etl::promise p(state); + etl::future f = p.get_future(); + CHECK(!f.is_ready()); + p.set_value(value); + CHECK(f.is_ready()); + CHECK(&f.get() == &value); + f.get() = 2; + CHECK_EQUAL(2, value); + + state.reset(); + CHECK(!f.is_ready()); + + int other = 3; + p.set_value(other); + CHECK(&f.get() == &other); + } + + //************************************************************************* + // Types + //************************************************************************* + + TEST(test_future_get_return_types) + { + CHECK((std::is_same&>().get())>::value)); + CHECK((std::is_same&>().get())>::value)); + CHECK((std::is_same&>().get())>::value)); + CHECK((std::is_same&>().get())>::value)); + } + + TEST(test_future_is_copyable) + { + CHECK(std::is_copy_constructible>::value); + CHECK(std::is_copy_assignable>::value); + + etl::shared_state state; + etl::future f1(state); + etl::future f2(f1); + state.set_value(5); + CHECK_EQUAL(5, f1.get()); + CHECK_EQUAL(5, f2.get()); + CHECK_EQUAL(5, f2.get()); // get() does not consume the value. + } + + TEST(test_promise_is_move_only) + { + CHECK(!std::is_copy_constructible>::value); + CHECK(!std::is_copy_assignable>::value); + CHECK(std::is_nothrow_move_constructible>::value); + CHECK(std::is_nothrow_move_assignable>::value); + + CHECK(!std::is_copy_constructible>::value); + CHECK(std::is_nothrow_move_constructible>::value); + + CHECK(!std::is_copy_constructible>::value); + CHECK(std::is_nothrow_move_constructible>::value); + } + + TEST(test_promise_move_construct) + { + etl::shared_state state; + etl::promise p1(state); + etl::promise p2(etl::move(p1)); + CHECK(!p1.valid()); + CHECK(p2.valid()); + + etl::future f = p2.get_future(); + p2.set_value(7); + CHECK_EQUAL(7, f.get()); + } + + TEST(test_promise_move_assign) + { + etl::shared_state state; + etl::promise p1(state); + etl::promise p2; + CHECK(!p2.valid()); + p2 = etl::move(p1); + CHECK(!p1.valid()); + CHECK(p2.valid()); + + etl::future f = p2.get_future(); + p2.set_value(); + CHECK(f.is_ready()); + } + + //************************************************************************* + // Errors + //************************************************************************* + + TEST(test_shared_state_get_not_ready) + { + etl::shared_state state; + const etl::shared_state& cstate = state; + etl::shared_state rstate; + CHECK_THROW((void)state.get(), etl::future_not_ready); + CHECK_THROW((void)cstate.get(), etl::future_not_ready); + CHECK_THROW((void)rstate.get(), etl::future_not_ready); + } + + TEST(test_shared_state_set_value_twice) + { + etl::shared_state state; + state.set_value(1); + CHECK_THROW(state.set_value(2), etl::promise_already_satisfied); + CHECK_EQUAL(1, state.get()); + + int a = 1; + int b = 2; + etl::shared_state rstate; + rstate.set_value(a); + CHECK_THROW(rstate.set_value(b), etl::promise_already_satisfied); + CHECK(&rstate.get() == &a); + + etl::shared_state vstate; + vstate.set_value(); + CHECK_THROW(vstate.set_value(), etl::promise_already_satisfied); + } + + TEST(test_promise_set_value_twice) + { + Counted::live = 0; + { + etl::shared_state state; + etl::promise p(state); + p.set_value(Counted(1)); + CHECK_THROW(p.set_value(Counted(2)), etl::promise_already_satisfied); + CHECK_EQUAL(1, state.get().value); + CHECK_EQUAL(1, Counted::live); + } + CHECK_EQUAL(0, Counted::live); + } + + TEST(test_future_get_not_ready) + { + etl::shared_state state; + etl::future f(state); + etl::shared_state vstate; + etl::future vf(vstate); + CHECK_THROW((void)f.get(), etl::future_not_ready); + CHECK_THROW(vf.get(), etl::future_not_ready); + } + + TEST(test_future_get_no_state) + { + etl::future f; + etl::future rf; + etl::future vf; + CHECK_THROW((void)f.get(), etl::future_no_state); + CHECK_THROW((void)rf.get(), etl::future_no_state); + CHECK_THROW(vf.get(), etl::future_no_state); + } + + TEST(test_promise_no_state) + { + int value = 1; + etl::promise p; + etl::promise rp; + etl::promise vp; + + CHECK_THROW((void)p.get_future(), etl::future_no_state); + CHECK_THROW(p.set_value(1), etl::future_no_state); + CHECK_THROW((void)rp.get_future(), etl::future_no_state); + CHECK_THROW(rp.set_value(value), etl::future_no_state); + CHECK_THROW((void)vp.get_future(), etl::future_no_state); + CHECK_THROW(vp.set_value(), etl::future_no_state); + } + + TEST(test_promise_no_state_after_move) + { + etl::shared_state state; + etl::promise p1(state); + etl::promise p2(etl::move(p1)); + CHECK_THROW(p1.set_value(1), etl::future_no_state); + CHECK(!state.is_ready()); + } + + TEST(test_exceptions_derive_from_future_exception) + { + etl::future f; + CHECK_THROW((void)f.get(), etl::future_exception); + + etl::promise p; + CHECK_THROW(p.set_value(1), etl::future_exception); + + etl::shared_state state; + state.set_value(1); + CHECK_THROW(state.set_value(2), etl::future_exception); + } + } +} // namespace + +#endif // ETL_HAS_ATOMIC diff --git a/test/vs2022/etl.vcxproj b/test/vs2022/etl.vcxproj index 226163fa..e7bb1ff5 100644 --- a/test/vs2022/etl.vcxproj +++ b/test/vs2022/etl.vcxproj @@ -10395,6 +10395,7 @@ + diff --git a/test/vs2022/etl.vcxproj.filters b/test/vs2022/etl.vcxproj.filters index b8b1f148..954b7de1 100644 --- a/test/vs2022/etl.vcxproj.filters +++ b/test/vs2022/etl.vcxproj.filters @@ -3587,6 +3587,9 @@ Tests\Types + + Tests\Misc + Tests\Patterns