Add extracted tools: CitrineOS, OpenOCPP, ShapeShifter
- CitrineOS core extracted (CSMS OCPP 2.0.1) - OpenOCPP extracted (firmware OCPP 1.6J/2.0.1) - ShapeShifter library installed (pip install -e) - ShapeShifter specification extracted - EVerest extracted TODO updated with progress
This commit is contained in:
@@ -0,0 +1,287 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
|
||||
/**
|
||||
* @file Wrapping of a resource and serializing the access to the resource
|
||||
* @brief The content is based on the conference talk 'Concurrency-and-Parallelism'
|
||||
* held by Herb Sutter at C-and-Beyond-2012.
|
||||
* The original pattern has been extend with exhaustive handling of corner cases and errors.
|
||||
* Policies allow for the adaptation to common use cases.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
#include <everest/util/queue/thread_safe_queue.hpp>
|
||||
#include <exception>
|
||||
#include <functional>
|
||||
#include <future>
|
||||
#include <memory>
|
||||
#include <thread>
|
||||
#include <type_traits>
|
||||
|
||||
namespace everest::lib::util::testing_interface {
|
||||
// Forward declaration of the test fixture used as a friend
|
||||
class AsyncWrapperTest;
|
||||
} // namespace everest::lib::util::testing_interface
|
||||
|
||||
namespace everest::lib::util {
|
||||
|
||||
// --- EXCEPTION POLICIES (Policy 1) ---
|
||||
|
||||
/**
|
||||
* @brief Policy that triggers a global shutdown if a user-supplied task throws an exception.
|
||||
* @details Used for guarded resources where an exception implies resource corruption.
|
||||
*/
|
||||
struct GlobalFailurePolicy {
|
||||
/**
|
||||
* @brief Sets the global promise with the given exception pointer, permanently failing the executor.
|
||||
*/
|
||||
static void handle_user_exception(std::shared_ptr<std::promise<void>> const& global_promise_ptr,
|
||||
std::exception_ptr current_exception_ptr) {
|
||||
try {
|
||||
global_promise_ptr->set_exception(current_exception_ptr);
|
||||
} catch (const std::future_error&) {
|
||||
// Ignore: promise was already satisfied by an earlier thread/task
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Policy that contains a user-supplied exception locally (does not cause global failure).
|
||||
* @details Used for background tasks where exceptions do not corrupt the resource state.
|
||||
*/
|
||||
struct LocalFailurePolicy {
|
||||
/**
|
||||
* @brief Handles the user exception by doing nothing to the global promise.
|
||||
*/
|
||||
static void handle_user_exception([[maybe_unused]] std::shared_ptr<std::promise<void>> const& global_promise_ptr,
|
||||
[[maybe_unused]] std::exception_ptr current_exception_ptr) {
|
||||
}
|
||||
};
|
||||
|
||||
// --- SHUTDOWN POLICIES (Policy 2) ---
|
||||
|
||||
/**
|
||||
* @brief Destructor policy that waits for all queued tasks to finish execution before joining the worker thread.
|
||||
*/
|
||||
struct WaitToFinishPolicy {
|
||||
/**
|
||||
* @brief Performs a graceful shutdown by pushing a sentinel task and joining.
|
||||
*/
|
||||
template <typename QueueT, typename ThreadT>
|
||||
static void shutdown(QueueT& queue, ThreadT& worker, std::atomic_bool& done_flag) {
|
||||
// Push the final stop signal, which will be executed after all pending tasks.
|
||||
queue.push([&done_flag] { done_flag = true; });
|
||||
worker.join();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Destructor policy that signals the worker to exit immediately, potentially dropping queued tasks.
|
||||
*/
|
||||
struct FastQuitPolicy {
|
||||
/**
|
||||
* @brief Performs an immediate shutdown by setting the flag and unblocking the thread.
|
||||
*/
|
||||
template <typename QueueT, typename ThreadT>
|
||||
static void shutdown(QueueT& queue, ThreadT& worker, std::atomic_bool& done_flag) {
|
||||
// Signal termination immediately
|
||||
done_flag = true;
|
||||
// Send a dummy task to unblock the worker if it's currently blocking on pop()
|
||||
queue.push([] {});
|
||||
worker.join();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief A single-threaded asynchronous executor that serializes access to a resource T.
|
||||
* @details All operations on T are performed sequentially on a dedicated worker thread. It uses two policies
|
||||
* (ExceptionPolicy and ShutdownPolicy) and a QueueT template parameter to define its entire contract.
|
||||
* @tparam T The resource type being wrapped.
|
||||
* @tparam ExceptionPolicy Defines global failure behavior (e.g., GlobalFailurePolicy).
|
||||
* @tparam ShutdownPolicy Defines destructor behavior (e.g., WaitToFinishPolicy).
|
||||
* @tparam QueueT The underlying thread-safe queue implementation.
|
||||
*/
|
||||
template <typename T, typename ExceptionPolicy, typename ShutdownPolicy,
|
||||
template <class> typename QueueT = thread_safe_queue>
|
||||
class async_wrapper_impl {
|
||||
public:
|
||||
friend class everest::lib::util::testing_interface::AsyncWrapperTest; ///< Allows GTest fixture to access
|
||||
///< protected/private members for testing.
|
||||
|
||||
using ThisT = async_wrapper_impl<T, ExceptionPolicy, ShutdownPolicy, QueueT>;
|
||||
|
||||
/**
|
||||
* @brief Constructs the wrapper, initializing the resource and starting the worker thread.
|
||||
*/
|
||||
template <class... ArgsT>
|
||||
explicit async_wrapper_impl(ArgsT&&... args) :
|
||||
m_resource(std::forward<ArgsT>(args)...), m_global_promise(std::make_shared<std::promise<void>>()) {
|
||||
|
||||
m_global_future = m_global_promise->get_future();
|
||||
|
||||
m_worker = std::thread([this] {
|
||||
while (not m_done) {
|
||||
try {
|
||||
m_queue.pop()(); // Execute the task lambda
|
||||
} catch (const std::exception& e) {
|
||||
|
||||
// Critical infrastructure failure handling
|
||||
try {
|
||||
m_global_promise->set_exception(
|
||||
std::make_exception_ptr(std::runtime_error("Async worker infrastructure failure.")));
|
||||
} catch (const std::future_error&) {
|
||||
// Ignore: Promise was already set by a concurrent thread/user task
|
||||
}
|
||||
m_done = true; // Signal thread termination
|
||||
} catch (...) {
|
||||
m_done = true; // Handle non-standard exceptions
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Rule of Five members
|
||||
async_wrapper_impl(ThisT&& other) noexcept = default;
|
||||
ThisT& operator=(ThisT&&) noexcept = default;
|
||||
async_wrapper_impl(ThisT const& other) = delete;
|
||||
|
||||
/**
|
||||
* @brief Destructor that shuts down the worker thread according to the ShutdownPolicy.
|
||||
*/
|
||||
~async_wrapper_impl() {
|
||||
ShutdownPolicy::shutdown(m_queue, m_worker, m_done);
|
||||
}
|
||||
|
||||
private:
|
||||
template <typename Fut, typename F> void promise_set_value(std::promise<Fut>& prom, F& f, T& t) const {
|
||||
prom.set_value(f(t));
|
||||
}
|
||||
|
||||
template <typename F> void promise_set_value(std::promise<void>& prom, F& f, T& t) const {
|
||||
f(t);
|
||||
prom.set_value();
|
||||
}
|
||||
|
||||
bool is_global_failure_signaled() const {
|
||||
return m_global_future.wait_for(std::chrono::seconds(0)) == std::future_status::ready;
|
||||
}
|
||||
|
||||
template <typename F, typename R> void enqueue_task(F&& f, std::shared_ptr<std::promise<R>> prom) const {
|
||||
auto global_promise_ptr = m_global_promise;
|
||||
|
||||
// --- SYNCHRONOUS FAILURE CHECK (Gatekeeper, executed on calling thread) ---
|
||||
if (is_global_failure_signaled()) {
|
||||
try {
|
||||
m_global_future.get();
|
||||
} catch (const std::exception&) {
|
||||
prom->set_exception(std::current_exception());
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
m_queue.push([f = std::forward<F>(f), prom, global_promise_ptr, this] {
|
||||
// --- ASYNCHRONOUS FAILURE CHECK (Mandatory Final Gatekeeper) ---
|
||||
if (is_global_failure_signaled()) {
|
||||
try {
|
||||
m_global_future.get();
|
||||
} catch (const std::exception&) {
|
||||
prom->set_exception(std::current_exception());
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
promise_set_value(*prom, f, m_resource);
|
||||
} catch (...) {
|
||||
prom->set_exception(std::current_exception());
|
||||
|
||||
ExceptionPolicy::handle_user_exception(global_promise_ptr, std::current_exception());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// --- ENQUEUE FOR FIRE-AND-FORGET TASKS FOR (No Promise, No Wait) ---
|
||||
template <typename F> void enqueue_task(F&& f) const {
|
||||
if (is_global_failure_signaled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto global_promise_ptr = m_global_promise;
|
||||
m_queue.push([f = std::forward<F>(f), global_promise_ptr, this] {
|
||||
if (is_global_failure_signaled()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
f(m_resource);
|
||||
} catch (...) {
|
||||
ExceptionPolicy::handle_user_exception(global_promise_ptr, std::current_exception());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
mutable T m_resource;
|
||||
mutable QueueT<std::function<void()>> m_queue;
|
||||
std::thread m_worker;
|
||||
std::atomic_bool m_done{false};
|
||||
std::shared_ptr<std::promise<void>> m_global_promise;
|
||||
std::shared_future<void> m_global_future;
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Submits a function object to the worker thread and returns a future for the result.
|
||||
* @details The function is guaranteed to be executed sequentially with respect to other tasks.
|
||||
* It receives the managed resource as it's only parameter.
|
||||
* @tparam F Function object callable with T&.
|
||||
* @return std::future<R> where R is the return type of F. The future will contain an exception
|
||||
* if the task fails or if a global failure signal has been set.
|
||||
*/
|
||||
template <typename F> auto operator()(F f) const {
|
||||
using ReturnT = decltype(f(m_resource));
|
||||
auto prom = std::make_shared<std::promise<ReturnT>>();
|
||||
auto fut = prom->get_future();
|
||||
|
||||
enqueue_task(std::forward<F>(f), prom);
|
||||
|
||||
return fut;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Submits a function object to the worker thread without returning a future (fire-and-forget).
|
||||
* @details This uses an optimized path that avoids creating a std::promise, relying only on the
|
||||
* ExceptionPolicy for failure handling.
|
||||
* @tparam F Function object callable with T&.
|
||||
*/
|
||||
template <typename F> void run(F f) const {
|
||||
enqueue_task<F>(std::forward<F>(f));
|
||||
}
|
||||
};
|
||||
|
||||
// --- TYPEDEFS ---
|
||||
|
||||
/** @brief Base alias for resources where a task failure causes GLOBAL corruption (Guarded). */
|
||||
template <typename T, typename ShutdownPolicy>
|
||||
using async_wrapper_guarded = async_wrapper_impl<T, GlobalFailurePolicy, ShutdownPolicy>;
|
||||
|
||||
/** @brief Intermediate base alias for resources where a task failure is LOCALIZED (Background). */
|
||||
template <typename T, typename ShutdownPolicy>
|
||||
using async_wrapper_local = async_wrapper_impl<T, LocalFailurePolicy, ShutdownPolicy>;
|
||||
|
||||
// Final Usage Types (Combine Exception Policy and Shutdown Policy)
|
||||
|
||||
/** * @brief Primary default wrapper: Local Failure (Background) with Fast Quit.
|
||||
* @tparam T The resource type.
|
||||
*/
|
||||
template <typename T> using async_wrapper = async_wrapper_local<T, FastQuitPolicy>;
|
||||
|
||||
/** @brief Guarded resource with graceful (wait-to-finish) shutdown. */
|
||||
template <typename T> using async_wrapper_guarded_wait = async_wrapper_guarded<T, WaitToFinishPolicy>;
|
||||
/** @brief Local resource with graceful (wait-to-finish) shutdown. */
|
||||
template <typename T> using async_wrapper_wait = async_wrapper_local<T, WaitToFinishPolicy>;
|
||||
/** @brief Guarded resource with fast (potentially lossy) shutdown. */
|
||||
template <typename T> using async_wrapper_guarded_fast = async_wrapper_guarded<T, FastQuitPolicy>;
|
||||
/** @brief Local resource with fast (potentially lossy) shutdown. */
|
||||
template <typename T> using async_wrapper_fast = async_wrapper_local<T, FastQuitPolicy>;
|
||||
|
||||
} // namespace everest::lib::util
|
||||
@@ -0,0 +1,295 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
|
||||
/**
|
||||
* @file monitor.hpp
|
||||
* @brief Provides a generic RAII Monitor pattern implementation for thread-safe access to a shared resource.
|
||||
*
|
||||
* The Monitor pattern bundles shared data with a synchronization mechanism (mutex and condition variable)
|
||||
* to ensure only one thread can access the data at any given time, and provides tools for thread
|
||||
* coordination (waiting and signaling).
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <condition_variable>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
|
||||
namespace everest::lib::util {
|
||||
|
||||
template <typename T, typename = void> struct has_arrow_operator : std::false_type {};
|
||||
|
||||
template <typename T>
|
||||
struct has_arrow_operator<T, std::void_t<decltype(std::declval<T>().operator->())>> : std::true_type {};
|
||||
|
||||
template <typename T> inline constexpr bool has_arrow_operator_v = has_arrow_operator<T>::value;
|
||||
|
||||
/**
|
||||
* @brief The RAII guard that provides locked access to the shared data T.
|
||||
* * This object is non-copyable but movable. Its existence guarantees that the
|
||||
* underlying mutex in the parent monitor object is held. When this handle
|
||||
* goes out of scope, the lock is automatically released.
|
||||
*
|
||||
* @tparam T The type of the protected resource.
|
||||
* @tparam MTX The mutex type used for locking (e.g., std::mutex, std::recursive_mutex).
|
||||
*/
|
||||
template <class T, class MTX> class monitor_handle {
|
||||
public:
|
||||
/**
|
||||
* @brief Constructs the monitor handle and takes ownership of the acquired lock.
|
||||
* @param obj Reference to the protected resource within the monitor.
|
||||
* @param mtx R-value reference to the acquired unique lock, ownership is moved.
|
||||
* @param cv Reference to the condition variable in the monitor.
|
||||
*/
|
||||
monitor_handle(T& obj, std::unique_lock<MTX>&& mtx, std::condition_variable& cv) :
|
||||
m_obj(obj), m_lock(std::move(mtx)), m_cv(cv) {
|
||||
}
|
||||
|
||||
// Monitor handles should not be copied, as that would duplicate a unique lock.
|
||||
monitor_handle(monitor_handle<T, MTX> const& other) = delete;
|
||||
monitor_handle<T, MTX>& operator=(monitor_handle<T, MTX> const& rhs) = delete;
|
||||
|
||||
// Defaulted move operations allow the handle to be moved (e.g., returned from monitor::handle).
|
||||
/** @brief generated default move constructor*/
|
||||
monitor_handle(monitor_handle<T, MTX>&& other) = default;
|
||||
/** @brief generated default move assignment*/
|
||||
monitor_handle<T, MTX>& operator=(monitor_handle<T, MTX>&& rhs) = default;
|
||||
|
||||
/**
|
||||
* @brief Destructor. Automatically releases the lock held by m_lock.
|
||||
*/
|
||||
~monitor_handle() = default;
|
||||
|
||||
/**
|
||||
* @brief Overloads the dereference operator to allow reference access to the protected object.
|
||||
* * This provides direct reference access to the guarded object T (or the wrapper T, e.g., std::unique_ptr<...>&).
|
||||
* The lock is held during the access.
|
||||
*
|
||||
* @return Reference to the protected object T.
|
||||
*/
|
||||
T& operator*() {
|
||||
return m_obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Overloads the arrow operator to provide unified pointer-like access to the protected object.
|
||||
* * This implementation uses C++17's `if constexpr` to support three primary access patterns:
|
||||
* 1. **Chaining (Returns T&):** Used when T is a pointer-like wrapper (e.g., std::unique_ptr<T>) or a raw pointer
|
||||
* (T*). This allows the compiler's built-in chaining mechanism to continue indirection until the final object is
|
||||
* reached.
|
||||
* 2. **Direct Pointer Access (Returns T*):** Used when T is the final object type (e.g., a simple struct or class).
|
||||
* This terminates the chain immediately with a pointer to the object.
|
||||
* * @note This method holds the mutex lock for the duration of the access.
|
||||
*
|
||||
* @return `decltype(auto)` returns T& for chaining/pointers, or T* for direct access.
|
||||
*/
|
||||
decltype(auto) operator->() {
|
||||
if constexpr (has_arrow_operator_v<T> || std::is_pointer_v<T>) {
|
||||
return m_obj;
|
||||
} else {
|
||||
return &m_obj;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Blocks the thread until the provided predicate returns true.
|
||||
* @details This function atomically releases the lock (allowing other threads to acquire it)
|
||||
* and waits for a notification on the condition variable. When woken, it re-acquires
|
||||
* the lock and re-checks the predicate.
|
||||
* * @note This method is only available when MTX is std::mutex.
|
||||
* @tparam Predicate The callable type (e.g., lambda) that returns a boolean.
|
||||
* @param pred The condition that must become true to stop waiting.
|
||||
*/
|
||||
template <class Predicate, class U = MTX, std::enable_if_t<std::is_same_v<U, std::mutex>>* = nullptr>
|
||||
void wait(Predicate&& pred) {
|
||||
m_cv.wait(m_lock, std::forward<Predicate>(pred));
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Blocks the thread until the predicate returns true or the timeout expires.
|
||||
* @note This method is only available when MTX is std::mutex.
|
||||
* @tparam Rep The type representing the duration count (e.g., int, long).
|
||||
* @tparam Period The type representing the duration period (e.g., std::milli, std::ratio<1>).
|
||||
* @tparam Predicate The callable type (e.g., lambda) that returns a boolean.
|
||||
* @param pred The condition that must become true to stop waiting.
|
||||
* @param timeout The maximum time to wait for the condition to become true.
|
||||
* @return true if the predicate became true, false if the timeout expired.
|
||||
*/
|
||||
template <class Rep, class Period, class Predicate, class U = MTX,
|
||||
std::enable_if_t<std::is_same_v<U, std::mutex>>* = nullptr>
|
||||
bool wait_for(Predicate&& pred, std::chrono::duration<Rep, Period> timeout) {
|
||||
return m_cv.wait_for(m_lock, timeout, std::forward<Predicate>(pred));
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Blocks the thread until the predicate returns true or the absolute time point is reached.
|
||||
* * If the predicate is false, the lock is atomically released, and the thread sleeps until
|
||||
* a notification is received or abs_time is reached. When woken, the lock is re-acquired
|
||||
* and the predicate is re-checked.
|
||||
* @note This method is only available when MTX is std::mutex.
|
||||
* @tparam Clock The clock type used for the time point (e.g., std::system_clock, std::steady_clock).
|
||||
* @tparam Duration The duration type used for the time point.
|
||||
* @tparam Predicate The callable type (e.g., lambda) that returns a boolean.
|
||||
* @param abs_time The absolute time point at which the wait will cease, regardless of predicate state.
|
||||
* @param pred The condition that must become true to stop waiting.
|
||||
* @return true if the predicate became true, false if the absolute time was reached.
|
||||
*/
|
||||
template <class Clock, class Duration, class Predicate, class U = MTX,
|
||||
std::enable_if_t<std::is_same_v<U, std::mutex>>* = nullptr>
|
||||
bool wait_until(std::chrono::time_point<Clock, Duration> const& abs_time, Predicate&& pred) {
|
||||
return m_cv.wait_until(m_lock, abs_time, std::forward<Predicate>(pred));
|
||||
}
|
||||
|
||||
private:
|
||||
T& m_obj; ///< Reference to the protected resource.
|
||||
std::unique_lock<MTX> m_lock; ///< The unique lock, holding the mutex during the handle's lifetime.
|
||||
std::condition_variable& m_cv; ///< Reference to the monitor's condition variable.
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief A generic monitor class that manages RAII access to its resource T.
|
||||
* * Provides thread-safe data encapsulation using a mutex and thread coordination
|
||||
* via a condition variable. Access to the internal resource T is only possible
|
||||
* by obtaining a monitor_handle.
|
||||
*
|
||||
* @tparam T The type of the resource being protected.
|
||||
* @tparam MTX The mutex type to use, defaults to std::mutex.
|
||||
*/
|
||||
template <class T, class MTX = std::mutex> class monitor {
|
||||
public:
|
||||
monitor() = default;
|
||||
/**
|
||||
* @brief Constructs the internal object T using move construction.
|
||||
*/
|
||||
explicit monitor(T&& obj) : m_obj(std::move(obj)) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Constructs the internal object T using perfect forwarding.
|
||||
* @tparam ArgsT Types of arguments used to construct T.
|
||||
* @param args Arguments passed to the constructor of T.
|
||||
*/
|
||||
template <class... ArgsT> explicit monitor(ArgsT&&... args) : m_obj(std::forward<ArgsT>(args)...) {
|
||||
}
|
||||
|
||||
~monitor() = default;
|
||||
|
||||
// Monitors should not be copied.
|
||||
monitor(monitor<T, MTX> const& other) = delete;
|
||||
monitor<T, MTX>& operator=(monitor<T, MTX> const& rhs) = delete;
|
||||
|
||||
/**
|
||||
* @brief Thread-safe move constructor. Locks the source mutex before swapping.
|
||||
* @details The move constructor is 'noexcept' if the monitor with it's template parameters
|
||||
* is no-throw swappable.
|
||||
*/
|
||||
monitor(monitor<T, MTX>&& other) noexcept(std::is_nothrow_swappable_v<T>) {
|
||||
// Lock the source monitor's mutex before moving its data to ensure thread safety
|
||||
std::unique_lock lock(other.m_mtx);
|
||||
// This pattern is important, don't just use std::swap, but enable std::swap for the case
|
||||
// no specialized optimazation is available. The following always prefers the the specialized version
|
||||
// via ADL lookup
|
||||
using std::swap;
|
||||
swap(m_obj, other.m_obj);
|
||||
// Note: m_mtx and m_cv are not swapped; they remain tied to the current object.
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Thread-safe move assignment operator. Locks the source mutex before swapping.
|
||||
* @details The move assignment operator is 'noexcept' if the monitor with it's template parameters
|
||||
* is no-throw swappable.
|
||||
* @return Reference to the current object.
|
||||
*/
|
||||
monitor<T, MTX>&
|
||||
operator=(monitor<T, MTX>&& rhs) noexcept(noexcept(std::declval<monitor&>().swap(std::declval<monitor&>()))) {
|
||||
if (this != &rhs) {
|
||||
this->swap(rhs);
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Blocks indefinitely to acquire the lock and return a handle.
|
||||
* @return monitor_handle<T, MTX> with ownership of the acquired lock.
|
||||
*/
|
||||
monitor_handle<T, MTX> handle() {
|
||||
std::unique_lock lock(m_mtx);
|
||||
return monitor_handle<T, MTX>(m_obj, std::move(lock), m_cv);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Attempts to acquire the lock within the specified timeout duration.
|
||||
* @note This method is only available when MTX is std::timed_mutex.
|
||||
* @tparam Rep The type representing the duration count.
|
||||
* @tparam Period The type representing the duration period.
|
||||
* @param timeout The maximum time to wait for the lock.
|
||||
* @return An optional handle: contains the handle if the lock was acquired, std::nullopt otherwise.
|
||||
*/
|
||||
template <class Rep, class Period, class U = MTX, std::enable_if_t<std::is_same_v<U, std::timed_mutex>>* = nullptr>
|
||||
std::optional<monitor_handle<T, MTX>> handle(std::chrono::duration<Rep, Period> timeout) {
|
||||
auto deadline = std::chrono::steady_clock::now() + timeout;
|
||||
|
||||
std::unique_lock lock(m_mtx, std::defer_lock);
|
||||
if (not lock.try_lock_until(deadline)) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
return monitor_handle<T, MTX>(m_obj, std::move(lock), m_cv);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Wakes up one thread currently waiting on the monitor's condition variable.
|
||||
* @note This method is only available when MTX is std::mutex.
|
||||
*/
|
||||
template <class U = MTX, std::enable_if_t<std::is_same_v<U, std::mutex>>* = nullptr> void notify_one() {
|
||||
m_cv.notify_one();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Wakes up all threads currently waiting on the monitor's condition variable.
|
||||
* @note This method is only available when MTX is std::mutex.
|
||||
*/
|
||||
template <class U = MTX, std::enable_if_t<std::is_same_v<U, std::mutex>>* = nullptr> void notify_all() {
|
||||
m_cv.notify_all();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Member swap function for thread-safe and exception-safe exchange of resources.
|
||||
* * Locks both mutexes using RAII and deadlock avoidance (via std::scoped_lock)
|
||||
* before swapping the protected resource T.
|
||||
* @details This function is 'noexcept' if T is no-throw swappable
|
||||
* @param other The monitor to swap resources with.
|
||||
*/
|
||||
void swap(monitor<T, MTX>& other) noexcept(std::is_nothrow_swappable_v<T>) {
|
||||
std::scoped_lock lock(m_mtx, other.m_mtx);
|
||||
// This pattern is important, don't just use std::swap, but enable std::swap for the case
|
||||
// no specialized optimazation is available. The following always prefers the the specialized version
|
||||
// via ADL lookup
|
||||
using std::swap;
|
||||
swap(m_obj, other.m_obj);
|
||||
}
|
||||
|
||||
private:
|
||||
MTX m_mtx; ///< The mutex protecting the resource T.
|
||||
T m_obj; ///< The protected resource.
|
||||
std::condition_variable m_cv; ///< The condition variable for thread coordination.
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Non-member swap function for standard ADL (Argument-Dependent Lookup) swap.
|
||||
* * This function delegates the call to the thread-safe member swap function, ensuring
|
||||
* a safe, deadlock-avoiding exchange of resources between two monitor objects.
|
||||
* * @tparam T The type of the resource being protected.
|
||||
* @details This function is 'noexcept' if the monitor with its template parameters is no-throw swappable
|
||||
* @tparam MTX The mutex type used for locking.
|
||||
* @param lhs The first monitor object.
|
||||
* @param rhs The second monitor object.
|
||||
*/
|
||||
template <class T, class MTX> void swap(monitor<T, MTX>& lhs, monitor<T, MTX>& rhs) noexcept(noexcept(lhs.swap(rhs))) {
|
||||
lhs.swap(rhs);
|
||||
}
|
||||
|
||||
} // namespace everest::lib::util
|
||||
@@ -0,0 +1,135 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2026 Pionix GmbH and Contributors to EVerest
|
||||
|
||||
/**
|
||||
* @file thread_pool.hpp
|
||||
* @brief Simple fixed-size thread pool implementation.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <everest/util/queue/thread_safe_queue.hpp>
|
||||
#include <functional>
|
||||
#include <future>
|
||||
#include <thread>
|
||||
|
||||
namespace everest::lib::util {
|
||||
|
||||
/**
|
||||
* @brief A thread safe fixed-size pool for task execution.
|
||||
* @details This pool maintains a constant number of worker threads. It provides two
|
||||
* interfaces for task submission: operator() for tasks requiring a return value
|
||||
* (via std::future) and run() for fire-and-forget tasks.
|
||||
*/
|
||||
class thread_pool {
|
||||
public:
|
||||
/** @brief Type definition for the tasks held in the queue. */
|
||||
using action = std::function<void()>;
|
||||
|
||||
/**
|
||||
* @brief Constructs the thread pool and spawns worker threads.
|
||||
* @param thread_count The number of worker threads to maintain.
|
||||
*/
|
||||
thread_pool(unsigned int thread_count) {
|
||||
auto worker_loop = [this] {
|
||||
while (auto task = m_action_queue.wait_and_pop()) {
|
||||
// Task successful, execute it while handling exceptions
|
||||
try {
|
||||
task.value()();
|
||||
} catch (...) {
|
||||
// Keep the worker alive even if the task fails.
|
||||
// Exceptions for operator() are handled in the promise wrapper.
|
||||
}
|
||||
}
|
||||
};
|
||||
for (std::size_t i = 0; i < thread_count; ++i) {
|
||||
m_threads.emplace_back(worker_loop);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Destructor. Signals all threads to stop and joins them.
|
||||
* @details Unblocks any threads waiting on the queue before joining.
|
||||
*/
|
||||
~thread_pool() {
|
||||
m_action_queue.stop();
|
||||
for (auto& elem : m_threads) {
|
||||
if (elem.joinable()) {
|
||||
elem.join();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Submits a task to the pool and returns a future for its result.
|
||||
* @tparam F The type of the callable.
|
||||
* @tparam Args The types of the arguments to pass to the callable.
|
||||
* @param f The callable to execute.
|
||||
* @param args The arguments to pass to the callable.
|
||||
* @return A std::future that will eventually contain the result of the callable.
|
||||
*/
|
||||
template <typename F, typename... Args>
|
||||
auto operator()(F&& f, Args&&... args) const -> std::future<std::invoke_result_t<F, Args...>> {
|
||||
using R = std::invoke_result_t<F, Args...>;
|
||||
|
||||
auto prom = std::make_shared<std::promise<R>>();
|
||||
auto fut = prom->get_future();
|
||||
enqueue_task(std::forward<F>(f), prom, std::forward<Args>(args)...);
|
||||
|
||||
return fut;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Submits a "fire-and-forget" task to the pool.
|
||||
* @details This method is highly efficient as it avoids the overhead of creating
|
||||
* std::promise and std::future objects. It returns immediately after the task
|
||||
* is added to the queue.
|
||||
* @tparam F The type of the callable.
|
||||
* @tparam Args The types of the arguments to pass to the callable.
|
||||
* @param f The callable to execute.
|
||||
* @param args The arguments to pass to the callable.
|
||||
*/
|
||||
template <typename F, typename... Args> void run(F&& f, Args&&... args) const {
|
||||
m_action_queue.push(std::bind(std::forward<F>(f), std::forward<Args>(args)...));
|
||||
}
|
||||
|
||||
private:
|
||||
/**
|
||||
* @brief Helper to set a promise value for non-void return types.
|
||||
*/
|
||||
template <typename Fut, typename F> static void promise_set_value(std::promise<Fut>& prom, F& f) {
|
||||
prom.set_value(f());
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Helper to set a promise value for void return types.
|
||||
*/
|
||||
template <typename F> static void promise_set_value(std::promise<void>& prom, F& f) {
|
||||
f();
|
||||
prom.set_value();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Wraps a task with a promise and enqueues it.
|
||||
*/
|
||||
template <typename F, typename... Args, typename R>
|
||||
void enqueue_task(F&& f, std::shared_ptr<std::promise<R>>& prom, Args&&... args) const {
|
||||
auto bound_f = std::bind(std::forward<F>(f), std::forward<Args>(args)...);
|
||||
m_action_queue.push([prom, task_f = std::move(bound_f)]() mutable {
|
||||
try {
|
||||
promise_set_value(*prom, task_f);
|
||||
} catch (...) {
|
||||
// Ensure promise is settled with an exception if task throws
|
||||
prom->set_exception(std::current_exception());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** @brief Thread safe queue for incoming tasks. */
|
||||
mutable thread_safe_queue<action> m_action_queue;
|
||||
|
||||
/** @brief Container for worker thread handles. */
|
||||
std::vector<std::thread> m_threads;
|
||||
};
|
||||
|
||||
} // namespace everest::lib::util
|
||||
@@ -0,0 +1,401 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2026 Pionix GmbH and Contributors to EVerest
|
||||
|
||||
/** \file */
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "everest/util/async/monitor.hpp"
|
||||
#include "everest/util/queue/thread_safe_bounded_queue.hpp"
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <functional>
|
||||
#include <future>
|
||||
#include <list>
|
||||
#include <optional>
|
||||
#include <thread>
|
||||
#include <type_traits>
|
||||
#include <variant>
|
||||
#include <vector>
|
||||
|
||||
namespace everest::lib::util {
|
||||
|
||||
/**
|
||||
* @brief A task wrapper that tracks when the task was enqueued.
|
||||
*/
|
||||
struct TrackedAction {
|
||||
std::function<void()> func; ///< The actual work to perform.
|
||||
std::chrono::steady_clock::time_point arrival; ///< Timestamp of enqueueing.
|
||||
|
||||
/**
|
||||
* @brief Constructs a tracked action with the current timestamp.
|
||||
* @param[in] f The functional object to be executed.
|
||||
*/
|
||||
explicit TrackedAction(std::function<void()> f) : func(std::move(f)), arrival(std::chrono::steady_clock::now()) {
|
||||
}
|
||||
};
|
||||
|
||||
// --- Scaling Policies ---
|
||||
|
||||
// A policy advertises whether it needs a background supervisor (and at what
|
||||
// cadence) via a single constexpr: `supervisor_tick`. `std::nullopt` means no
|
||||
// supervisor; a value means "re-evaluate scaling every <tick> ms".
|
||||
|
||||
/**
|
||||
* @brief Greedy scaling policy: grows whenever there is any backlog.
|
||||
*/
|
||||
struct GreedyScaling {
|
||||
static constexpr std::optional<std::chrono::milliseconds> supervisor_tick = std::nullopt;
|
||||
/**
|
||||
* @brief Decides to grow if there is any backlog.
|
||||
* @param current_workers Number of threads currently in the registry.
|
||||
* @param queue_size Number of tasks waiting in the queue.
|
||||
* @return true if we should spawn a new thread.
|
||||
*/
|
||||
static bool should_grow([[maybe_unused]] std::size_t current_workers, std::size_t queue_size,
|
||||
std::optional<std::chrono::steady_clock::time_point> oldest_task) {
|
||||
// If queue_size > 1, it means even if a worker is currently
|
||||
// popping, there is at least one other task that will be stuck waiting.
|
||||
return queue_size > 1;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Conservative scaling policy: grows only when backlog is significant.
|
||||
*/
|
||||
struct ConservativeScaling {
|
||||
static constexpr std::optional<std::chrono::milliseconds> supervisor_tick = std::nullopt;
|
||||
static bool should_grow(std::size_t current_workers, std::size_t queue_size,
|
||||
[[maybe_unused]] std::optional<std::chrono::steady_clock::time_point> oldest_task) {
|
||||
return queue_size > (current_workers * 2);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Fixed size scaling policy: grows after a specific queue depth limit is reached.
|
||||
* @tparam Limit The queue size threshold.
|
||||
*/
|
||||
template <std::size_t Limit> struct FixedSizeScaling {
|
||||
static constexpr std::optional<std::chrono::milliseconds> supervisor_tick = std::nullopt;
|
||||
static bool should_grow([[maybe_unused]] std::size_t current_workers, std::size_t queue_size,
|
||||
[[maybe_unused]] std::optional<std::chrono::steady_clock::time_point> oldest_arrival) {
|
||||
return queue_size >= Limit;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Latency-based scaling policy: grows if the oldest task has waited too long.
|
||||
* @tparam ThresholdMs Maximum tolerable wait time in milliseconds.
|
||||
* @tparam TickMs Cadence at which the supervisor re-evaluates the policy.
|
||||
*/
|
||||
template <std::size_t ThresholdMs = 10, std::size_t TickMs = 5> struct LatencyScaling {
|
||||
static constexpr std::optional<std::chrono::milliseconds> supervisor_tick = std::chrono::milliseconds(TickMs);
|
||||
static bool should_grow([[maybe_unused]] std::size_t current_workers, std::size_t queue_size,
|
||||
std::optional<std::chrono::steady_clock::time_point> oldest_arrival) {
|
||||
if (queue_size < 1 or not oldest_arrival.has_value()) {
|
||||
return false;
|
||||
}
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
const auto wait = std::chrono::duration_cast<std::chrono::milliseconds>(now - oldest_arrival.value());
|
||||
return wait.count() > static_cast<long long>(ThresholdMs);
|
||||
}
|
||||
};
|
||||
|
||||
// --- Thread Pool ---
|
||||
|
||||
// +--------------+--------------------------+----------------------------------------+
|
||||
// | POLICY | GROWTH TRIGGER LOGIC | CHARACTER / INTENT |
|
||||
// +--------------+--------------------------+----------------------------------------+
|
||||
// | Greedy | queue_size > 1 | Minimizes latency at all costs. Scales |
|
||||
// | | | the moment a backlog is detected. |
|
||||
// +--------------+--------------------------+----------------------------------------+
|
||||
// | Latency | wait > ThresholdMs | Balances resources with SLA. Scales |
|
||||
// | | | only if tasks sit too long in queue. |
|
||||
// +--------------+--------------------------+----------------------------------------+
|
||||
// | Conservative | queue_size > (workers*2) | Prioritizes stability. Scales only |
|
||||
// | | | when tasks significantly outnumber |
|
||||
// | | | current worker capacity. |
|
||||
// +--------------+--------------------------+----------------------------------------+
|
||||
// | FixedSize | queue_size >= Limit | Rigid and predictable. Grows only |
|
||||
// | | | when a specific depth limit is hit. |
|
||||
// +--------------+--------------------------+----------------------------------------+
|
||||
|
||||
// --- Exception Handling Policies ---
|
||||
|
||||
/**
|
||||
* @brief Exception policy: silently swallow exceptions (fire-and-forget semantics).
|
||||
*/
|
||||
struct SuppressExceptions {
|
||||
static void handle_exception([[maybe_unused]] std::exception_ptr) noexcept {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Exception policy: rethrow from the worker thread, terminating the process if uncaught.
|
||||
*/
|
||||
struct RethrowExceptions {
|
||||
[[noreturn]] static void handle_exception(std::exception_ptr eptr) {
|
||||
std::rethrow_exception(eptr);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief A thread pool that dynamically scales its worker count based on a policy.
|
||||
* * @details This pool maintains a minimum number of threads and expands up to a maximum
|
||||
* when the ScalingPolicy (e.g., LatencyScaling or GreedyScaling) signals that growth
|
||||
* is necessary. Idle surplus threads are automatically retired after a specified timeout.
|
||||
* * @tparam ScalingPolicy A policy class implementing should_grow(size_t, size_t, std::optional<time_point>).
|
||||
* * @tparam ExceptionPolicy A policy class implementing a static handle_exception() called inside the catch block.
|
||||
*/
|
||||
template <typename ScalingPolicy = LatencyScaling<10>, typename ExceptionPolicy = SuppressExceptions>
|
||||
class thread_pool_scaling {
|
||||
public:
|
||||
using action = std::function<void()>;
|
||||
|
||||
/**
|
||||
* @brief Constructs the scalable thread pool.
|
||||
* @tparam Rep The representation type of the duration.
|
||||
* @tparam Period The period type of the duration.
|
||||
* @param[in] min Minimum worker threads to keep alive.
|
||||
* @param[in] max Maximum allowed worker threads.
|
||||
* @param[in] timeout Idle duration before a surplus worker retires. Defaults to 60s.
|
||||
* @param[in] queue_limit Maximum tasks allowed in the queue. Defaults to 0 (unbounded).
|
||||
*
|
||||
* The supervisor tick (if any) is carried by the ScalingPolicy itself; see
|
||||
* @ref LatencyScaling for an example.
|
||||
*/
|
||||
template <class Rep, class Period>
|
||||
thread_pool_scaling(std::size_t min, std::size_t max,
|
||||
std::chrono::duration<Rep, Period> timeout = std::chrono::seconds(60),
|
||||
std::size_t queue_limit = 0) :
|
||||
m_min_threads(min),
|
||||
m_max_threads(max),
|
||||
m_idle_timeout(std::chrono::duration_cast<std::chrono::milliseconds>(timeout)),
|
||||
m_action_queue(queue_limit) {
|
||||
|
||||
{
|
||||
auto reg_h = m_reg.handle();
|
||||
for (std::size_t i = 0; i < m_min_threads; ++i) {
|
||||
spawn_worker_internal(reg_h);
|
||||
}
|
||||
}
|
||||
if constexpr (ScalingPolicy::supervisor_tick.has_value()) {
|
||||
m_supervisor = std::thread([this] { run_supervisor(*ScalingPolicy::supervisor_tick); });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Destructor. Signals shutdown and joins all active worker threads.
|
||||
*/
|
||||
~thread_pool_scaling() {
|
||||
// 1. Signal shutdown and wake the supervisor + producers/consumers.
|
||||
{
|
||||
auto reg_h = m_reg.handle();
|
||||
reg_h->shutdown = true;
|
||||
}
|
||||
m_reg.notify_all();
|
||||
m_action_queue.stop();
|
||||
|
||||
// 2. Join the supervisor before tearing down the worker list: the supervisor
|
||||
// can spawn new workers, and we must not race with the steal in step 3.
|
||||
if constexpr (ScalingPolicy::supervisor_tick.has_value()) {
|
||||
if (m_supervisor.joinable()) {
|
||||
m_supervisor.join();
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Steal the active workers list. Explicitly clear the source so that any
|
||||
// worker that acquires the lock afterwards sees size()==0 and cannot
|
||||
// voluntarily retire into the zombies deque after step 5's final reap.
|
||||
std::list<std::thread> workers_to_join;
|
||||
{
|
||||
auto reg_h = m_reg.handle();
|
||||
workers_to_join = std::move(reg_h->workers);
|
||||
reg_h->workers.clear();
|
||||
}
|
||||
|
||||
// 4. Join everything in our stolen list
|
||||
for (auto& t : workers_to_join) {
|
||||
if (t.joinable()) {
|
||||
t.join();
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Join any zombies that retired before the steal. Steal the deque first
|
||||
// so the join happens outside the lock (same pattern as the worker loop).
|
||||
std::deque<std::thread> zombies_to_join;
|
||||
{
|
||||
auto reg_h = m_reg.handle();
|
||||
zombies_to_join = std::move(reg_h->zombies);
|
||||
}
|
||||
for (auto& t : zombies_to_join) {
|
||||
if (t.joinable()) {
|
||||
t.join();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Submits a "fire-and-forget" task for execution.
|
||||
* @details Optimized path that avoids promise/future overhead.
|
||||
* @param[in] f The task to execute.
|
||||
* @param[in] args Arguments to pass to the task.
|
||||
*/
|
||||
template <typename F, typename... Args> void run(F&& f, Args&&... args) {
|
||||
submit_to_queue(std::bind(std::forward<F>(f), std::forward<Args>(args)...));
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Submits a task and returns a future for the result.
|
||||
* @return A std::future containing the result of the task.
|
||||
*/
|
||||
template <typename F, typename... Args>
|
||||
auto operator()(F&& f, Args&&... args) -> std::future<std::invoke_result_t<F, Args...>> {
|
||||
using R = std::invoke_result_t<F, Args...>;
|
||||
auto prom = std::make_shared<std::promise<R>>();
|
||||
auto fut = prom->get_future();
|
||||
|
||||
submit_to_queue([prom, bound_f = std::bind(std::forward<F>(f), std::forward<Args>(args)...)]() mutable {
|
||||
try {
|
||||
if constexpr (std::is_void_v<R>) {
|
||||
bound_f();
|
||||
prom->set_value();
|
||||
} else {
|
||||
prom->set_value(bound_f());
|
||||
}
|
||||
} catch (...) {
|
||||
prom->set_exception(std::current_exception());
|
||||
}
|
||||
});
|
||||
return fut;
|
||||
}
|
||||
|
||||
private:
|
||||
/**
|
||||
* @brief Data structure representing the internal state of worker management.
|
||||
*/
|
||||
struct RegistryData {
|
||||
std::list<std::thread> workers; ///< List of active worker threads.
|
||||
std::deque<std::thread> zombies; ///< Threads that have exited but not yet been joined.
|
||||
bool shutdown = false; ///< Global shutdown flag.
|
||||
};
|
||||
|
||||
using handle = monitor_handle<RegistryData, std::mutex>; ///< Alias for monitor access.
|
||||
|
||||
/**
|
||||
* @brief Internal helper to push tasks and trigger the scaling heuristic.
|
||||
* @param[in] func The functional object to enqueue.
|
||||
*/
|
||||
void submit_to_queue(action&& func) {
|
||||
std::size_t size_after_push = m_action_queue.push(TrackedAction(std::move(func)));
|
||||
auto oldest_arrival = m_action_queue.oldest_arrival();
|
||||
|
||||
if (size_after_push > 0) {
|
||||
auto reg_h = m_reg.handle();
|
||||
if (reg_h->workers.size() < m_max_threads &&
|
||||
ScalingPolicy::should_grow(reg_h->workers.size(), size_after_push, oldest_arrival)) {
|
||||
spawn_worker_internal(reg_h);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Spawns a new worker thread.
|
||||
* @param[in] reg_h Handle to the monitor-protected registry data.
|
||||
*/
|
||||
void spawn_worker_internal(handle& reg_h) {
|
||||
reg_h->workers.emplace_back();
|
||||
auto it = std::prev(reg_h->workers.end());
|
||||
|
||||
*it = std::thread([this, it]() {
|
||||
while (true) {
|
||||
auto task_opt = m_action_queue.try_pop(m_idle_timeout);
|
||||
if (task_opt) {
|
||||
try {
|
||||
task_opt->func();
|
||||
} catch (...) {
|
||||
ExceptionPolicy::handle_exception(std::current_exception());
|
||||
}
|
||||
// Steal the zombie deque under the lock, then join outside it.
|
||||
// Joining while holding the lock is safe in practice (the zombie has already
|
||||
// released the lock before it can appear in the deque), but it blocks the
|
||||
// registry mutex for the duration of the join — delaying scaling decisions
|
||||
// and the destructor. Stealing first bounds the critical section to a cheap
|
||||
// list move.
|
||||
std::deque<std::thread> zombies_to_join;
|
||||
{
|
||||
auto reg_h = m_reg.handle();
|
||||
if (!reg_h->zombies.empty()) {
|
||||
zombies_to_join = std::move(reg_h->zombies);
|
||||
}
|
||||
}
|
||||
for (auto& t : zombies_to_join) {
|
||||
if (t.joinable()) {
|
||||
t.join();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
auto reg_h = m_reg.handle();
|
||||
|
||||
// 1. THE CRITICAL CHECK:
|
||||
// If shutdown is true, the destructor has already moved (or is moving)
|
||||
// the 'workers' list. We must NOT touch 'it' or the 'workers' list.
|
||||
if (reg_h->shutdown) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. VOLUNTARY RETIREMENT:
|
||||
// This only executes if we are NOT shutting down.
|
||||
// Since we are holding the monitor lock and shutdown is false,
|
||||
// we know 'it' is still valid in reg_h->workers.
|
||||
if (reg_h->workers.size() > m_min_threads) {
|
||||
reg_h->zombies.push_back(std::move(*it));
|
||||
reg_h->workers.erase(it);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Supervisor loop. Periodically re-evaluates the scaling policy so that
|
||||
* time-based policies (e.g. LatencyScaling) scale up when tasks sit in the queue
|
||||
* without any new submission to trigger a check.
|
||||
*/
|
||||
void run_supervisor(std::chrono::milliseconds tick) {
|
||||
while (true) {
|
||||
auto reg_h = m_reg.handle();
|
||||
// wait_for returns true when the predicate is satisfied (shutdown requested),
|
||||
// false on timeout.
|
||||
if (reg_h.wait_for([&]() { return reg_h->shutdown; }, tick)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const std::size_t queue_size = m_action_queue.size();
|
||||
if (queue_size == 0) {
|
||||
continue;
|
||||
}
|
||||
if (reg_h->workers.size() >= m_max_threads) {
|
||||
continue;
|
||||
}
|
||||
const auto oldest_arrival = m_action_queue.oldest_arrival();
|
||||
if (ScalingPolicy::should_grow(reg_h->workers.size(), queue_size, oldest_arrival)) {
|
||||
spawn_worker_internal(reg_h);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const std::size_t m_min_threads; ///< Minimum persistent thread count.
|
||||
const std::size_t m_max_threads; ///< Maximum allowed thread count.
|
||||
const std::chrono::milliseconds m_idle_timeout; ///< Surplus thread idle timeout.
|
||||
|
||||
thread_safe_bounded_queue<TrackedAction> m_action_queue; ///< Task queue.
|
||||
monitor<RegistryData> m_reg; ///< Worker registry.
|
||||
/// Background scaling supervisor. Only materialized as a real `std::thread`
|
||||
/// for policies whose `supervisor_tick` has a value; otherwise collapses to
|
||||
/// a `std::monostate` so non-supervisor pools don't carry a dead thread handle.
|
||||
std::conditional_t<ScalingPolicy::supervisor_tick.has_value(), std::thread, std::monostate> m_supervisor;
|
||||
};
|
||||
|
||||
} // namespace everest::lib::util
|
||||
@@ -0,0 +1,244 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2024 Pionix GmbH and Contributors to EVerest
|
||||
|
||||
/**
|
||||
* \file convert an enum into bit flags
|
||||
* \note enum must contain item "last" which has the highest value
|
||||
*/
|
||||
|
||||
#ifndef ENUMFLAGS_HPP
|
||||
#define ENUMFLAGS_HPP
|
||||
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
#include <limits>
|
||||
#include <type_traits>
|
||||
|
||||
namespace everest::lib::util {
|
||||
|
||||
/**
|
||||
* \brief templated class to use an enumeration as bit flags
|
||||
* \note Enumeration must have the last element called last
|
||||
*
|
||||
* Example:
|
||||
* \code
|
||||
* enum class example : std::uin8_t {
|
||||
* item1,
|
||||
* item2,
|
||||
* item3,
|
||||
* last = item3,
|
||||
* };
|
||||
*
|
||||
* util::EnumFlags<example> flags;
|
||||
*
|
||||
* flags.set(example::item1);
|
||||
* flags.is_set(example::item1); // true
|
||||
* flags.reset(example::item1);
|
||||
* flags.is_set(example::item1); // false
|
||||
* \endcode
|
||||
*
|
||||
* Multiple flags can be combined:
|
||||
* \code
|
||||
* flags.reset();
|
||||
* flags.set(example::item1, example::item2);
|
||||
* flags.is_set(example::item1, example::item2); // true
|
||||
* flags.is_set(example::item3, example::item2); // false
|
||||
* flags.is_any_set(example::item3, example::item2); // true
|
||||
* \endcode
|
||||
*/
|
||||
|
||||
template <typename T>
|
||||
using SelectedUInt = std::conditional_t<
|
||||
(static_cast<std::size_t>(T::last) < 8), std::uint8_t,
|
||||
std::conditional_t<(static_cast<std::size_t>(T::last) < 16), std::uint16_t,
|
||||
std::conditional_t<(static_cast<std::size_t>(T::last) < 32), std::uint32_t,
|
||||
std::conditional_t<(static_cast<std::size_t>(T::last) < 64), std::uint64_t,
|
||||
void // invalid, triggers static_assert below
|
||||
>>>>;
|
||||
|
||||
template <typename T, typename B> class EnumFlagsBase {
|
||||
public:
|
||||
static_assert(std::is_enum<T>(), "Not enum");
|
||||
static_assert(std::is_integral<SelectedUInt<T>>(), "Not supported");
|
||||
|
||||
private:
|
||||
B _value{0ULL};
|
||||
|
||||
constexpr auto max_value() const {
|
||||
if constexpr (static_cast<std::underlying_type_t<T>>(T::last) == 64) {
|
||||
return std::numeric_limits<std::uint64_t>::max();
|
||||
} else {
|
||||
return (1ULL << (static_cast<std::underlying_type_t<T>>(T::last) + 1)) - 1;
|
||||
}
|
||||
}
|
||||
|
||||
public:
|
||||
/**
|
||||
* \brief return the bit position for the specified enum value
|
||||
* \param[in] flag the enum value
|
||||
* \returns an unsigned integer with the equivalent bit set
|
||||
*/
|
||||
static constexpr std::size_t bit(T flag) {
|
||||
return 1ULL << static_cast<std::underlying_type_t<T>>(flag);
|
||||
}
|
||||
|
||||
/**
|
||||
* \brief set the state of a specific flag
|
||||
* \param[in] flag - the enum value to update
|
||||
* \param[in] value - set/reset the flag
|
||||
*/
|
||||
constexpr void set(T flag, bool value) {
|
||||
if (value) {
|
||||
set(flag);
|
||||
} else {
|
||||
reset(flag);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* \brief set the specific flag
|
||||
* \param[in] flag - the enum value to set
|
||||
*/
|
||||
constexpr void set(T flag) {
|
||||
_value |= bit(flag);
|
||||
}
|
||||
|
||||
/**
|
||||
* \brief set flags to a specific value
|
||||
* \note not recommended
|
||||
*/
|
||||
constexpr void set(std::underlying_type_t<T> v) {
|
||||
_value = v & max_value();
|
||||
}
|
||||
|
||||
/**
|
||||
* \brief same as above but for multiple flags
|
||||
*/
|
||||
template <typename... Flags> constexpr void set(T flag, const Flags... flags) {
|
||||
set(flag);
|
||||
set(flags...);
|
||||
}
|
||||
|
||||
/**
|
||||
* \brief reset the specific flag
|
||||
* \param[in] flag - the enum value to reset
|
||||
*/
|
||||
constexpr void reset(T flag) {
|
||||
_value &= ~bit(flag);
|
||||
}
|
||||
|
||||
/**
|
||||
* \brief same as above but for multiple flags
|
||||
*/
|
||||
template <typename... Flags> constexpr void reset(T flag, const Flags... flags) {
|
||||
reset(flag);
|
||||
reset(flags...);
|
||||
}
|
||||
|
||||
/**
|
||||
* \brief reset all flags
|
||||
*/
|
||||
constexpr void reset() {
|
||||
_value = 0ULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* \brief set all flags
|
||||
*/
|
||||
constexpr void set() {
|
||||
_value = max_value();
|
||||
}
|
||||
|
||||
/**
|
||||
* \brief test if all flags are reset (i.e. 0)
|
||||
* \returns true when no flag is set
|
||||
*/
|
||||
[[nodiscard]] constexpr bool all_reset() const {
|
||||
return _value == 0ULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* \brief test if any flags are set
|
||||
* \returns true when any flag is set
|
||||
*/
|
||||
[[nodiscard]] constexpr bool any_reset() const {
|
||||
return _value != max_value();
|
||||
}
|
||||
|
||||
/**
|
||||
* \brief test if all flags are reset (i.e. 0)
|
||||
* \returns true when no flag is set
|
||||
*/
|
||||
[[nodiscard]] constexpr bool all_set() const {
|
||||
return _value == max_value();
|
||||
}
|
||||
|
||||
/**
|
||||
* \brief test if any flags are set
|
||||
* \returns true when any flag is set
|
||||
*/
|
||||
[[nodiscard]] constexpr bool any_set() const {
|
||||
return _value != 0ULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* \brief test if a flag is set
|
||||
* \returns true when the flag is set
|
||||
*/
|
||||
constexpr bool is_set(T flag) const {
|
||||
return (_value & bit(flag)) != 0;
|
||||
}
|
||||
constexpr bool is_any_set(T flag) const {
|
||||
return is_set(flag);
|
||||
}
|
||||
|
||||
/**
|
||||
* \brief test if a flag is reset
|
||||
* \returns true when the flag is reset
|
||||
*/
|
||||
constexpr bool is_reset(T flag) const {
|
||||
return (_value & bit(flag)) == 0;
|
||||
}
|
||||
constexpr bool is_any_reset(T flag) const {
|
||||
return is_reset(flag);
|
||||
}
|
||||
|
||||
/**
|
||||
* \brief retrieve all flags
|
||||
* \returns the internal _value
|
||||
*/
|
||||
constexpr auto get() const {
|
||||
return _value;
|
||||
}
|
||||
|
||||
/**
|
||||
* \brief same as above but for multiple flags
|
||||
* \note is_set() all specified flags must be set for a true result
|
||||
* \note is_any_set() at least one of the specified flags must be set for a true result
|
||||
* \note is_reset() all specified flags must be reset for a true result
|
||||
* \note is_any_reset() at least one of the specified flags must be reset for a true result
|
||||
*/
|
||||
template <typename... Flags> constexpr bool is_set(T flag, const Flags... flags) const {
|
||||
return is_set(flag) && is_set(flags...);
|
||||
}
|
||||
|
||||
template <typename... Flags> constexpr bool is_any_set(T flag, const Flags... flags) const {
|
||||
return is_any_set(flag) || is_any_set(flags...);
|
||||
}
|
||||
|
||||
template <typename... Flags> constexpr bool is_reset(T flag, const Flags... flags) const {
|
||||
return is_reset(flag) && is_reset(flags...);
|
||||
}
|
||||
|
||||
template <typename... Flags> constexpr bool is_any_reset(T flag, const Flags... flags) const {
|
||||
return is_any_reset(flag) || is_any_reset(flags...);
|
||||
}
|
||||
};
|
||||
|
||||
template <typename T> struct EnumFlags : public EnumFlagsBase<T, SelectedUInt<T>> {};
|
||||
|
||||
template <typename T> struct AtomicEnumFlags : public EnumFlagsBase<T, std::atomic<SelectedUInt<T>>> {};
|
||||
|
||||
} // namespace everest::lib::util
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,234 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
|
||||
// Originally from https://github.com/EVerest/libfsm/blob/draft/v2/include/fsm/v2/fsm.hpp
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace fsm::v2 {
|
||||
|
||||
namespace detail {
|
||||
|
||||
// detection idiom, see https://blog.tartanllama.xyz/detection-idiom/
|
||||
template <typename T, typename = void> struct output_type {
|
||||
using type = void;
|
||||
};
|
||||
|
||||
template <typename T> struct output_type<T, std::void_t<decltype(std::declval<T>().output)>> {
|
||||
using type = decltype(std::declval<T>().output);
|
||||
};
|
||||
|
||||
template <typename T> using output_type_t = typename output_type<T>::type;
|
||||
|
||||
// clang-format off
|
||||
template <typename, typename = void> constexpr bool is_template_state_compliant = false;
|
||||
|
||||
template <typename T>
|
||||
constexpr bool is_template_state_compliant<
|
||||
T, std::void_t<
|
||||
// Check for a 'ContainerType' used by the FSM/NestedFSM
|
||||
typename T::ContainerType,
|
||||
// Check for a 'EventType' used by the 'feed' function
|
||||
typename T::EventType,
|
||||
// Check for functions enter/feed/leave/get_id
|
||||
decltype(std::declval<T>().enter()), decltype(std::declval<T>().feed(std::declval<typename T::EventType>())),
|
||||
decltype(std::declval<T>().leave()), decltype(std::declval<T>().get_id()),
|
||||
// TODO(ioan): also check for types of this members?
|
||||
// Check that the return of 'feed' has the 'new_state' and 'unhandled' members
|
||||
decltype(std::declval<T>().feed(std::declval<typename T::EventType>()).unhandled),
|
||||
decltype(std::declval<T>().feed(std::declval<typename T::EventType>()).new_state)>> = true;
|
||||
// clang-format on
|
||||
|
||||
struct FeedResult {
|
||||
|
||||
FeedResult() = default;
|
||||
FeedResult(bool transition) : m_state(transition ? State::Transition : State::NoTransition) {
|
||||
}
|
||||
|
||||
operator bool() const {
|
||||
return m_state != State::Unhandled;
|
||||
}
|
||||
|
||||
bool transitioned() const {
|
||||
return (m_state == State::Transition);
|
||||
}
|
||||
|
||||
private:
|
||||
enum class State {
|
||||
Unhandled,
|
||||
Transition,
|
||||
NoTransition,
|
||||
};
|
||||
State m_state{State::Unhandled};
|
||||
};
|
||||
} // namespace detail
|
||||
|
||||
template <typename OutputType> struct FeedResult : public detail::FeedResult {
|
||||
using detail::FeedResult::FeedResult;
|
||||
|
||||
FeedResult(OutputType feed_output, bool transition) :
|
||||
detail::FeedResult(transition), output(std::move(feed_output)) {
|
||||
}
|
||||
|
||||
OutputType output; // we're requiring this to be default constructible?
|
||||
};
|
||||
|
||||
template <> struct FeedResult<void> : public detail::FeedResult {
|
||||
using detail::FeedResult::FeedResult; // inherit ctors
|
||||
};
|
||||
|
||||
template <typename StateType> class AbstractFSM {
|
||||
static_assert(detail::is_template_state_compliant<StateType>,
|
||||
"State must define a 'using EventType'! "
|
||||
"State must define a 'using ContainerType'! "
|
||||
"State must implement 'enter', 'feed', 'leave' functions! "
|
||||
"Return of 'feed' must have the 'unhandled' and 'new_state' members! ");
|
||||
};
|
||||
|
||||
template <typename StateType> class FSM : public AbstractFSM<StateType> {
|
||||
public:
|
||||
using StateContainerType = typename StateType::ContainerType;
|
||||
|
||||
FSM(StateContainerType initial_state) : m_current_state(std::move(initial_state)) {
|
||||
m_current_state->enter();
|
||||
}
|
||||
|
||||
~FSM() {
|
||||
m_current_state->leave();
|
||||
}
|
||||
|
||||
template <typename... Args> auto feed(Args&&... args) {
|
||||
auto result = m_current_state->feed(std::forward<Args>(args)...);
|
||||
|
||||
using OutputType = detail::output_type_t<decltype(result)>;
|
||||
|
||||
if (result.unhandled) {
|
||||
return FeedResult<OutputType>();
|
||||
}
|
||||
|
||||
const bool transitioned = (result.new_state != nullptr);
|
||||
|
||||
if (transitioned) {
|
||||
m_current_state->leave();
|
||||
m_current_state = std::move(result.new_state);
|
||||
m_current_state->enter();
|
||||
}
|
||||
|
||||
if constexpr (std::is_same_v<OutputType, void>) {
|
||||
return FeedResult<OutputType>(transitioned);
|
||||
} else {
|
||||
return FeedResult<OutputType>(std::move(result.output), transitioned);
|
||||
}
|
||||
}
|
||||
|
||||
auto get_current_state_id() const {
|
||||
return m_current_state->get_id();
|
||||
}
|
||||
|
||||
private:
|
||||
StateContainerType m_current_state;
|
||||
};
|
||||
|
||||
namespace detail {
|
||||
template <typename StateStackType> void unroll_child_states(StateStackType& state_stack) {
|
||||
while (true) {
|
||||
auto& leaf = state_stack.back();
|
||||
leaf->enter();
|
||||
|
||||
auto child = leaf->get_initial();
|
||||
|
||||
if (child == nullptr) {
|
||||
break;
|
||||
}
|
||||
|
||||
state_stack.emplace_back(std::move(child));
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace detail
|
||||
|
||||
template <typename StateType> class NestedFSM : public AbstractFSM<StateType> {
|
||||
public:
|
||||
using StateContainerType = typename StateType::ContainerType;
|
||||
|
||||
NestedFSM(StateContainerType initial_state) {
|
||||
m_state_stack.emplace_back(std::move(initial_state));
|
||||
detail::unroll_child_states(m_state_stack);
|
||||
}
|
||||
|
||||
~NestedFSM() {
|
||||
// leave all the states in order
|
||||
for (auto leaf = m_state_stack.rbegin(); leaf != m_state_stack.rend();) {
|
||||
(*leaf)->leave();
|
||||
leaf = std::next(leaf);
|
||||
m_state_stack.pop_back();
|
||||
}
|
||||
}
|
||||
|
||||
template <typename... Args> auto feed(Args&&... args) {
|
||||
auto leaf = m_state_stack.rbegin();
|
||||
|
||||
auto result = (*leaf)->feed(std::forward<Args>(args)...);
|
||||
using OutputType = detail::output_type_t<decltype(result)>;
|
||||
|
||||
auto next_state = std::next(leaf);
|
||||
|
||||
// descend stack as long as no state handles the event or the
|
||||
// stack's bottom is reached
|
||||
while (result.unhandled and (next_state != m_state_stack.rend())) {
|
||||
// check next state on stack
|
||||
// FIXME (aw): this will break with rvalue reference, as
|
||||
// they are moved multiple times
|
||||
result = (*next_state)->feed(std::forward<Args>(args)...);
|
||||
next_state = std::next(next_state);
|
||||
}
|
||||
|
||||
// if not handled at all, don't do anything at all
|
||||
if (result.unhandled) {
|
||||
return FeedResult<OutputType>();
|
||||
}
|
||||
|
||||
const bool transitioned = (result.new_state != nullptr);
|
||||
|
||||
if (transitioned) {
|
||||
while (leaf != next_state) {
|
||||
(*leaf)->leave();
|
||||
leaf = std::next(leaf);
|
||||
|
||||
m_state_stack.pop_back();
|
||||
}
|
||||
|
||||
m_state_stack.emplace_back(std::move(result.new_state));
|
||||
detail::unroll_child_states(m_state_stack);
|
||||
}
|
||||
|
||||
if constexpr (std::is_same_v<OutputType, void>) {
|
||||
return FeedResult<OutputType>(transitioned);
|
||||
} else {
|
||||
return FeedResult<OutputType>(std::move(result.output), transitioned);
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE (aw): could be computed along the feed
|
||||
auto get_current_state_id() const {
|
||||
using StateIDType = decltype(std::declval<StateType>().get_id());
|
||||
|
||||
std::vector<StateIDType> current_ids;
|
||||
current_ids.reserve(m_state_stack.size());
|
||||
|
||||
for (const auto& state : m_state_stack) {
|
||||
current_ids.emplace_back(state->get_id());
|
||||
}
|
||||
|
||||
return current_ids;
|
||||
}
|
||||
|
||||
private:
|
||||
std::vector<StateContainerType> m_state_stack;
|
||||
};
|
||||
|
||||
} // namespace fsm::v2
|
||||
@@ -0,0 +1,190 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2022 - 2026 Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
#include <optional>
|
||||
#include <type_traits>
|
||||
|
||||
/**
|
||||
* @file comparison.hpp
|
||||
* @brief Mathematical and Optional utility functions for the EVerest framework.
|
||||
*/
|
||||
|
||||
namespace everest::lib::util {
|
||||
|
||||
// --- Floating Point Utilities ---
|
||||
|
||||
/**
|
||||
* @brief Calculates a threshold based on powers of ten (10^-n).
|
||||
* * * Positive `digits_of_precision` generate fractional limits (e.g., 3 yields 0.001).
|
||||
* * Negative `digits_of_precision` generate magnitude limits (e.g., -2 yields 100.0).
|
||||
* * A value of 0 yields 1.0.
|
||||
* * @tparam T The floating point type (float, double, long double).
|
||||
* @param digits_of_precision The exponent modifier for the threshold.
|
||||
* @return constexpr T The calculated threshold value.
|
||||
*/
|
||||
template <class T, std::enable_if_t<std::is_floating_point_v<T>>* = nullptr>
|
||||
constexpr T range_limit(int digits_of_precision) {
|
||||
T result = static_cast<T>(1.0);
|
||||
|
||||
if (digits_of_precision > 0) {
|
||||
// Handle fractions (10^-n)
|
||||
for (int i = 0; i < digits_of_precision; ++i) {
|
||||
result /= static_cast<T>(10.0);
|
||||
}
|
||||
} else if (digits_of_precision < 0) {
|
||||
// Handle magnitudes (10^+n)
|
||||
for (int i = 0; i > digits_of_precision; --i) {
|
||||
result *= static_cast<T>(10.0);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Compares two floating point numbers for equality within a decimal precision.
|
||||
* * Uses a fixed epsilon (absolute difference) approach. This is ideal for
|
||||
* physical values (like Amps or Volts) where the scale of the numbers is
|
||||
* relatively consistent and known.
|
||||
* * @tparam Prec The number of decimal digits of precision to use for comparison.
|
||||
* @tparam T1, T2 Floating point types of the input values.
|
||||
* @param lhs Left hand side value.
|
||||
* @param rhs Right hand side value.
|
||||
* @return true if the absolute difference is less than 10^-Prec.
|
||||
*/
|
||||
template <int Prec, class T1, class T2,
|
||||
std::enable_if_t<std::is_floating_point_v<T1> && std::is_floating_point_v<T2>>* = nullptr>
|
||||
constexpr bool almost_eq(T1 lhs, T2 rhs) {
|
||||
using Common = std::common_type_t<T1, T2>;
|
||||
const auto val_lhs = static_cast<Common>(lhs);
|
||||
const auto val_rhs = static_cast<Common>(rhs);
|
||||
// std::abs is not guaranteed to be constexpr in C++17, using the ternary operate instead.
|
||||
const auto diff = (val_lhs > val_rhs) ? (val_lhs - val_rhs) : (val_rhs - val_lhs);
|
||||
return diff < range_limit<Common>(Prec);
|
||||
}
|
||||
/**
|
||||
* @brief Compares two std::optional floating point values for equality.
|
||||
* * Logic follows these rules:
|
||||
* 1. If both contain values, performs almost_eq on the underlying values.
|
||||
* 2. If both are empty, they are considered equal (true).
|
||||
* 3. If only one is empty, they are not equal (false).
|
||||
* * @tparam Prec Decimal digits of precision.
|
||||
* @param lhs Optional value A.
|
||||
* @param rhs Optional value B.
|
||||
*/
|
||||
template <int Prec, class T> constexpr bool almost_eq(std::optional<T> const& lhs, std::optional<T> const& rhs) {
|
||||
if (lhs.has_value() and rhs.has_value()) {
|
||||
return almost_eq<Prec>(lhs.value(), rhs.value());
|
||||
}
|
||||
return lhs.has_value() == rhs.has_value();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Determines if the difference between two values is within a specific noise level.
|
||||
* * Useful for filtering out jitter in sensor readings or small fluctuations
|
||||
* that should not trigger logic changes.
|
||||
* * @param val_a First value.
|
||||
* @param val_b Second value.
|
||||
* @param noise_level The maximum allowed difference to be considered "noise".
|
||||
* @return true if |val_a - val_b| <= noise_level.
|
||||
*/
|
||||
template <class T, std::enable_if_t<std::is_floating_point_v<T>>* = nullptr>
|
||||
bool in_noise_range(T val_a, T val_b, T noise_level) {
|
||||
return std::abs(val_a - val_b) <= noise_level;
|
||||
}
|
||||
|
||||
// --- Optional Math Utilities (Minimum) ---
|
||||
|
||||
/**
|
||||
* @brief Finds the minimum between two std::optional values.
|
||||
* * In this context, an empty std::optional is treated as "no limit" or "infinity".
|
||||
* * @param a First optional value.
|
||||
* @param b Second optional value.
|
||||
* @return The smaller of the two if both exist, otherwise the existing value.
|
||||
*/
|
||||
template <class T, std::enable_if_t<std::is_floating_point_v<T>>* = nullptr>
|
||||
std::optional<T> min_optional(std::optional<T> const& a, std::optional<T> const& b) {
|
||||
if (not a.has_value()) {
|
||||
return b;
|
||||
}
|
||||
if (not b.has_value()) {
|
||||
return a;
|
||||
}
|
||||
return std::min(a.value(), b.value());
|
||||
}
|
||||
|
||||
template <class T> T min_optional(T a, std::optional<T> const& b) {
|
||||
if (b.has_value() and b.value() < a) {
|
||||
return b.value();
|
||||
}
|
||||
return a;
|
||||
}
|
||||
|
||||
template <class T> T min_optional(std::optional<T> const& a, T b) {
|
||||
if (a.has_value() and a.value() < b) {
|
||||
return a.value();
|
||||
}
|
||||
return b;
|
||||
}
|
||||
|
||||
// --- Optional Math Utilities (Maximum) ---
|
||||
|
||||
/**
|
||||
* @brief Finds the maximum between two std::optional values.
|
||||
* * In this context, an empty std::optional is treated as "negative infinity".
|
||||
* * @param a First optional value.
|
||||
* @param b Second optional value.
|
||||
* @return The larger of the two if both exist, otherwise the existing value.
|
||||
*/
|
||||
template <class T, std::enable_if_t<std::is_floating_point_v<T>>* = nullptr>
|
||||
std::optional<T> max_optional(std::optional<T> const& a, std::optional<T> const& b) {
|
||||
if (not a.has_value()) {
|
||||
return b;
|
||||
}
|
||||
if (not b.has_value()) {
|
||||
return a;
|
||||
}
|
||||
return std::max(a.value(), b.value());
|
||||
}
|
||||
|
||||
template <class T> T max_optional(T a, std::optional<T> const& b) {
|
||||
if (b.has_value() and b.value() > a) {
|
||||
return b.value();
|
||||
}
|
||||
return a;
|
||||
}
|
||||
|
||||
template <class T> T max_optional(std::optional<T> const& a, T b) {
|
||||
if (a.has_value() and a.value() > b) {
|
||||
return a.value();
|
||||
}
|
||||
return b;
|
||||
}
|
||||
|
||||
// --- Optional Math Utilities (Clamping) ---
|
||||
|
||||
/**
|
||||
* @brief Clamps a value between an optional minimum and an optional maximum.
|
||||
* * If a bound is empty, it is ignored (e.g., if min is empty, only the max is applied).
|
||||
* * @param v The value to clamp.
|
||||
* @param min The lower bound constraint.
|
||||
* @param max The upper bound constraint.
|
||||
* @return The clamped value.
|
||||
*/
|
||||
template <class T> T clamp_optional(T v, std::optional<T> const& min, std::optional<T> const& max) {
|
||||
T result = v;
|
||||
if (min.has_value() and result < min.value()) {
|
||||
result = min.value();
|
||||
}
|
||||
if (max.has_value() and result > max.value()) {
|
||||
result = max.value();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace everest::lib::util
|
||||
@@ -0,0 +1,66 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2026 Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
|
||||
namespace everest::lib::util {
|
||||
/**
|
||||
* @brief Bind member function to std::function
|
||||
* @param[in] func Member function
|
||||
* @param[in] ptr to object (likely this)
|
||||
*/
|
||||
|
||||
template <typename R, typename C, typename... Args>
|
||||
std::function<R(Args...)> function_bind_obj(R (C::*func)(Args...), C* obj) {
|
||||
return [func, obj](Args... args) -> R { return (obj->*func)(std::forward<Args>(args)...); };
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Binds a member function to a specific object instance without std::function overhead.
|
||||
*
|
||||
* This utility creates a lightweight lambda closure that captures a member function pointer
|
||||
* and an object pointer.
|
||||
*
|
||||
* @tparam R The return type of the member function.
|
||||
* @tparam C The class type owning the member function.
|
||||
* @tparam Args The argument types expected by the member function.
|
||||
*
|
||||
* @param func A pointer to the member function (e.g., &MyClass::handle_data).
|
||||
* @param obj A pointer to the instance the function should be called on.
|
||||
*
|
||||
* @return A lambda closure that, when called, invokes the member function on @p obj.
|
||||
*
|
||||
* @note By returning 'auto', this function avoids the type-erasure overhead of std::function.
|
||||
* The compiler can often inline the resulting call entirely. It remains implicitly
|
||||
* convertible to std::function if required by an API.
|
||||
*
|
||||
* @par Example:
|
||||
* @code
|
||||
* auto processor = bind_obj(&MyCalss::handle_data, this);
|
||||
* consume(queue, processor);
|
||||
* @endcode
|
||||
*/
|
||||
template <typename R, typename C, typename... Args> auto bind_obj(R (C::*func)(Args...), C* obj) {
|
||||
return [func, obj](Args&&... args) -> R { return (obj->*func)(std::forward<Args>(args)...); };
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Const-qualified version of bind_obj.
|
||||
*
|
||||
* Overload to support binding to member functions marked with 'const'.
|
||||
* @tparam R The return type of the member function.
|
||||
* @tparam C The class type owning the member function.
|
||||
* @tparam Args The argument types expected by the member function.
|
||||
*
|
||||
* @param func A pointer to the const member function.
|
||||
* @param obj A pointer to the const instance.
|
||||
*
|
||||
* @return A lambda closure that invokes the const member function on @p obj.
|
||||
*/
|
||||
template <typename R, typename C, typename... Args> auto bind_obj(R (C::*func)(Args...) const, const C* obj) {
|
||||
return [func, obj](Args&&... args) -> R { return (obj->*func)(std::forward<Args>(args)...); };
|
||||
}
|
||||
|
||||
} // namespace everest::lib::util
|
||||
@@ -0,0 +1,128 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
#include <iterator>
|
||||
#include <optional>
|
||||
#include <type_traits>
|
||||
|
||||
/**
|
||||
* @file container_utils.hpp
|
||||
* @brief Utility functions for generic STL container operations.
|
||||
*/
|
||||
|
||||
namespace everest::lib::util {
|
||||
|
||||
/**
|
||||
* @brief Internal type trait to detect if a container has a .find() member function.
|
||||
* @tparam T The container type to check.
|
||||
* @tparam Value The type of the value to search for.
|
||||
*/
|
||||
template <typename T, typename Value, typename = void> struct has_find : std::false_type {};
|
||||
|
||||
template <typename T, typename Value>
|
||||
struct has_find<T, Value, std::void_t<decltype(std::declval<const T&>().find(std::declval<const Value&>()))>>
|
||||
: std::true_type {};
|
||||
|
||||
/**
|
||||
* @brief Checks if a value exists in a sequence container (vector, list, etc.).
|
||||
* @note Internal implementation using O(n) linear search.
|
||||
*/
|
||||
template <typename Container, typename T> auto exists_impl(const Container& c, const T& val, std::false_type) -> bool {
|
||||
return std::find(std::begin(c), std::end(c), val) != std::end(c);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Checks if a value exists in an associative container (set, map, etc.).
|
||||
* @note Internal implementation using the container's optimized .find() method.
|
||||
*/
|
||||
template <typename Container, typename T> auto exists_impl(const Container& c, const T& val, std::true_type) -> bool {
|
||||
return c.find(val) != c.end();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Generically checks if a specific item exists within a container.
|
||||
* * This function automatically selects the most efficient search algorithm
|
||||
* available for the provided container type at compile-time.
|
||||
* This is supposed to be a replacement for C++20 'contains' method
|
||||
* * @tparam Container The type of the STL-compatible container.
|
||||
* @tparam T The type of the value to search for.
|
||||
* @param c The constant reference to the container to search.
|
||||
* @param val The value to look for.
|
||||
* @return true if the item is found, false otherwise.
|
||||
* * @par Complexity:
|
||||
* - **O(log n)** for associative containers (std::set, std::map).
|
||||
* - **O(1)** average for unordered containers (std::unordered_set/map).
|
||||
* - **O(n)** for sequence containers (std::vector, std::list).
|
||||
*/
|
||||
template <typename Container, typename T> bool exists(const Container& c, const T& val) {
|
||||
return exists_impl(c, val, has_find<Container, T>{});
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Implementation for sequence containers (vector, list, etc.).
|
||||
* @return Pointer to the found element or nullptr.
|
||||
*/
|
||||
template <typename Container, typename T> auto find_ptr_impl(Container& c, const T& val, std::false_type) {
|
||||
auto it = std::find(std::begin(c), std::end(c), val);
|
||||
return (it != std::end(c)) ? &(*it) : nullptr;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Implementation for associative containers (set, map, etc.).
|
||||
* @return Pointer to the found element or nullptr.
|
||||
*/
|
||||
template <typename Container, typename T> auto find_ptr_impl(Container& c, const T& val, std::true_type) {
|
||||
auto it = c.find(val);
|
||||
return (it != std::end(c)) ? &(*it) : nullptr;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Generically finds an item and returns a pointer to it.
|
||||
* * This utility provides a unified interface to find an element across different
|
||||
* STL containers. It returns a pointer to the element if found, allowing
|
||||
* for both existence checking and immediate access.
|
||||
* * @tparam Container The STL container type.
|
||||
* @tparam T The type of the value/key to search for.
|
||||
* @param c The container (can be const or non-const).
|
||||
* @param val The value to search for.
|
||||
* @return A pointer to the element within the container, or `nullptr` if not found.
|
||||
* * @example
|
||||
* if (auto* item = utils::find_ptr(my_vector, 42)) {
|
||||
* *item = 43; // Modify if non-const
|
||||
* }
|
||||
*/
|
||||
template <typename Container, typename T> auto find_ptr(Container& c, const T& val) {
|
||||
return find_ptr_impl(c, val, has_find<Container, T>{});
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Searches for an element and returns an optional iterator.
|
||||
* * This function abstracts the difference between sequence containers (like vector)
|
||||
* and associative containers (like set/map) to provide the most efficient
|
||||
* lookup possible.
|
||||
* * @tparam Container The STL container type.
|
||||
* @tparam T The value or key to search for.
|
||||
* @param c The container to search within.
|
||||
* @param val The value/key to find.
|
||||
* @return std::optional wrapping the iterator. Returns std::nullopt if not found.
|
||||
* * @note If found, the iterator can be used to access the element (*it) or
|
||||
* pass to c.erase(it) for efficient deletion.
|
||||
*/
|
||||
template <typename Container, typename T>
|
||||
auto find_optional(Container& c, const T& val) -> std::optional<decltype(std::begin(c))> {
|
||||
if constexpr (has_find<Container, T>::value) {
|
||||
auto it = c.find(val);
|
||||
if (it != std::end(c))
|
||||
return it;
|
||||
} else {
|
||||
auto it = std::find(std::begin(c), std::end(c), val);
|
||||
if (it != std::end(c))
|
||||
return it;
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
} // namespace everest::lib::util
|
||||
@@ -0,0 +1,113 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
|
||||
/** \file */
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <optional>
|
||||
#include <queue>
|
||||
|
||||
namespace everest::lib::util {
|
||||
|
||||
/**
|
||||
* Simplified interface for a queue based
|
||||
* on <a href="https://en.cppreference.com/w/cpp/container/queue">std::queue</a>
|
||||
* @tparam T Datatype held by the queue
|
||||
*/
|
||||
template <class T> class simple_queue {
|
||||
public:
|
||||
/**
|
||||
* @var reference
|
||||
* @brief Inherited type definition from STL
|
||||
*/
|
||||
using reference = typename std::queue<T>::reference;
|
||||
|
||||
/**
|
||||
* @var const_reference
|
||||
* @brief Inherited type definition from STL
|
||||
*/
|
||||
using const_reference = typename std::queue<T>::const_reference;
|
||||
|
||||
/**
|
||||
* @var value_type
|
||||
* @brief Inherited type definition from STL
|
||||
*/
|
||||
using value_type = typename std::queue<T>::value_type;
|
||||
|
||||
/**
|
||||
* @var size_type
|
||||
* @brief Inherited type definition from STL
|
||||
*/
|
||||
using size_type = typename std::queue<T>::size_type;
|
||||
|
||||
/**
|
||||
* @brief Maps to std::queue::front()
|
||||
*/
|
||||
const_reference front() const {
|
||||
return m_queue.front();
|
||||
}
|
||||
|
||||
/**
|
||||
* Non-const front() is crucial for move semantics
|
||||
**/
|
||||
reference front() {
|
||||
return m_queue.front();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Maps to std::queue::back()
|
||||
*/
|
||||
const_reference back() const {
|
||||
return m_queue.back();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Maps to std::queue::push(const value_type&)
|
||||
*/
|
||||
void push(const value_type& value) {
|
||||
m_queue.push(value);
|
||||
}
|
||||
/**
|
||||
* @brief Maps to std::queue::push(value_type&&)
|
||||
*/
|
||||
void push(value_type&& value) {
|
||||
m_queue.push(std::forward<value_type>(value));
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get the front element of the queue if available.
|
||||
* @return Maybe the front element
|
||||
*/
|
||||
std::optional<value_type> pop() {
|
||||
if (m_queue.empty()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
value_type tmp = std::move(front());
|
||||
m_queue.pop();
|
||||
return tmp;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Maps to std::queue::empty()
|
||||
*/
|
||||
bool empty() const {
|
||||
return m_queue.empty();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Maps to std::queue::size()
|
||||
*/
|
||||
size_type size() const {
|
||||
return m_queue.size();
|
||||
}
|
||||
|
||||
template <class... Args> decltype(auto) emplace(Args&&... args) {
|
||||
m_queue.emplace(std::forward<Args>(args)...);
|
||||
}
|
||||
|
||||
private:
|
||||
std::queue<T> m_queue;
|
||||
};
|
||||
|
||||
} // namespace everest::lib::util
|
||||
@@ -0,0 +1,197 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2026 Pionix GmbH and Contributors to EVerest
|
||||
|
||||
/** \file */
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "simple_queue.hpp"
|
||||
#include <chrono>
|
||||
#include <condition_variable>
|
||||
#include <mutex>
|
||||
|
||||
namespace everest::lib::util {
|
||||
|
||||
/**
|
||||
* A thread safe bounded queue implemented on top of \ref queue::simple_queue. <br>
|
||||
* The common resource \ref simple_queue is guarded by a mutex in every member function.
|
||||
* A caller blocking on \p push will be unblocked when space becomes available via \p pop.
|
||||
* A caller blocking on \p pop or \p try_pop will be unblocked when new data is made available via \p push.
|
||||
* @tparam T Datatype held by the queue
|
||||
*/
|
||||
template <class T> class thread_safe_bounded_queue {
|
||||
public:
|
||||
/**
|
||||
* @var value_type
|
||||
* @brief Inherited type definition.
|
||||
*/
|
||||
using value_type = typename simple_queue<T>::value_type;
|
||||
|
||||
/**
|
||||
* @var size_type
|
||||
* @brief Inherited size type definition.
|
||||
*/
|
||||
using size_type = typename simple_queue<T>::size_type;
|
||||
|
||||
/**
|
||||
* @brief Constructor for the bounded queue.
|
||||
* @param[in] max_size The maximum number of elements allowed in the queue.
|
||||
* A value of 0 indicates an unbounded queue.
|
||||
*/
|
||||
explicit thread_safe_bounded_queue(size_type max_size = 0) : m_max_size(max_size) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Push new data into the queue
|
||||
* @details Blocks the caller if the queue has reached its \p max_size.
|
||||
* @param[in] value data
|
||||
* @return The size of the queue after push. Returns 0 if the queue is stopped.
|
||||
*/
|
||||
size_type push(const value_type& value) {
|
||||
std::unique_lock lock(m_mtx);
|
||||
if (m_max_size > 0) {
|
||||
m_cv_producer.wait(lock, [this]() { return m_queue.size() < m_max_size || m_stop; });
|
||||
}
|
||||
|
||||
if (m_stop) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
m_queue.push(value);
|
||||
auto result = m_queue.size();
|
||||
lock.unlock();
|
||||
m_cv_consumer.notify_one();
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Push new data into the queue
|
||||
* @details Blocks the caller if the queue has reached its \p max_size.
|
||||
* @param[in] value data
|
||||
* @return The size of the queue after push. Returns 0 if the queue is stopped.
|
||||
*/
|
||||
size_type push(value_type&& value) {
|
||||
std::unique_lock lock(m_mtx);
|
||||
if (m_max_size > 0) {
|
||||
m_cv_producer.wait(lock, [this]() { return m_queue.size() < m_max_size || m_stop; });
|
||||
}
|
||||
|
||||
if (m_stop) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
m_queue.push(std::forward<value_type>(value));
|
||||
auto result = m_queue.size();
|
||||
lock.unlock();
|
||||
m_cv_consumer.notify_one();
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Try to get an element from the queue.
|
||||
* @details Returns immediately.
|
||||
* @return An element from the queue, if one is available. \p std::nullopt otherwise
|
||||
*/
|
||||
std::optional<value_type> try_pop() {
|
||||
return pop_impl(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Try to get an element from the queue.
|
||||
* @details Returns as soon as data is availble or after timeout.
|
||||
* @param[in] timeout as <a href="https://en.cppreference.com/w/cpp/chrono/duration">std::chrono::duration</a>.
|
||||
* Smallest unit acceptable is milliseconds.
|
||||
* @return An element from the queue, if one is available. \p std::nullopt otherwise
|
||||
*/
|
||||
template <class Rep, class Period> std::optional<value_type> try_pop(std::chrono::duration<Rep, Period> timeout) {
|
||||
auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(timeout);
|
||||
return pop_impl(ms.count());
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get an element from the queue
|
||||
* @details Only returns, when data is available. Implicitly throws on stop().
|
||||
* @return An element from the queue.
|
||||
*/
|
||||
value_type pop() {
|
||||
return pop_impl(-1).value();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get an element from the queue
|
||||
* @details Only returns, when data is available, or the queue is stopped.
|
||||
* @return An element from the queue. Empty optional if stopped.
|
||||
*/
|
||||
std::optional<value_type> wait_and_pop() {
|
||||
return pop_impl(-1);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Signals that no more items will be pushed and unblocks all waiting consumers and producers.
|
||||
* @details Remaining items in the queue can still be popped until it is empty.
|
||||
*/
|
||||
void stop() {
|
||||
std::unique_lock lock(m_mtx);
|
||||
m_stop = true;
|
||||
lock.unlock();
|
||||
m_cv_consumer.notify_all();
|
||||
m_cv_producer.notify_all();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Safely returns the arrival time of the oldest task.
|
||||
* @return std::optional containing the time_point of the oldest task,
|
||||
* or std::nullopt if the queue is empty.
|
||||
*/
|
||||
std::optional<std::chrono::steady_clock::time_point> oldest_arrival() const {
|
||||
std::lock_guard lock(m_mtx);
|
||||
if (m_queue.empty()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
return m_queue.front().arrival;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Safely returns the current number of elements in the queue.
|
||||
*/
|
||||
size_type size() const {
|
||||
std::lock_guard lock(m_mtx);
|
||||
return m_queue.size();
|
||||
}
|
||||
|
||||
private:
|
||||
/**
|
||||
* @brief Internal implementation of the pop logic.
|
||||
* @param[in] timeout_ms Timeout in milliseconds. -1 for infinite wait, 0 for immediate return.
|
||||
* @return An optional containing the popped value or std::nullopt.
|
||||
*/
|
||||
std::optional<value_type> pop_impl(int timeout_ms) {
|
||||
std::unique_lock lock(m_mtx);
|
||||
auto wait_predicate = [this]() { return not m_queue.empty() or m_stop; };
|
||||
|
||||
if (timeout_ms < 0) {
|
||||
m_cv_consumer.wait(lock, wait_predicate);
|
||||
} else if (timeout_ms > 0) {
|
||||
(void)m_cv_consumer.wait_for(lock, std::chrono::milliseconds(timeout_ms), wait_predicate);
|
||||
}
|
||||
|
||||
// if the queue is still empty, we return a nullopt. Note that this would be implicitly
|
||||
// handled by simple_queue::pop, but it is added here to be more explcit
|
||||
if (m_queue.empty()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
auto result = m_queue.pop();
|
||||
lock.unlock();
|
||||
m_cv_producer.notify_one();
|
||||
return result;
|
||||
}
|
||||
|
||||
simple_queue<T> m_queue; ///< The underlying non-thread-safe container.
|
||||
const size_type m_max_size; ///< Maximum capacity of the queue.
|
||||
mutable std::mutex m_mtx; ///< Mutex guarding access to the queue and state.
|
||||
std::condition_variable m_cv_consumer; ///< Condition variable for consumers waiting for data.
|
||||
std::condition_variable m_cv_producer; ///< Condition variable for producers waiting for space.
|
||||
bool m_stop{false}; ///< Flag indicating the queue is shutting down.
|
||||
};
|
||||
} // namespace everest::lib::util
|
||||
@@ -0,0 +1,131 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
|
||||
/** \file */
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "simple_queue.hpp"
|
||||
#include <chrono>
|
||||
#include <condition_variable>
|
||||
#include <mutex>
|
||||
|
||||
namespace everest::lib::util {
|
||||
|
||||
/**
|
||||
* A thread safe queue implemented on top of \ref queue::simple_queue. <br>
|
||||
* The common resource \ref simple_queue is guarded by a mutex in every member function. A caller blocking on \p pop
|
||||
* or \p try_pop will be unblocked when new data made available via \p push
|
||||
* @tparam T Datatype held by the queue
|
||||
*/
|
||||
template <class T> class thread_safe_queue {
|
||||
public:
|
||||
/**
|
||||
* @var value_type
|
||||
* @brief Inherited type definition.
|
||||
*/
|
||||
using value_type = typename simple_queue<T>::value_type;
|
||||
|
||||
/**
|
||||
* @brief Push new data into the queue
|
||||
* @param[in] value data
|
||||
* @return The size of the queue after push
|
||||
*/
|
||||
typename simple_queue<T>::size_type push(const value_type& value) {
|
||||
std::unique_lock lock(m_mtx);
|
||||
m_queue.push(value);
|
||||
auto result = m_queue.size();
|
||||
lock.unlock();
|
||||
m_cv.notify_one();
|
||||
return result;
|
||||
}
|
||||
/**
|
||||
* @brief Push new data into the queue
|
||||
* @param[in] value data
|
||||
* @return The size of the queue after push
|
||||
*/
|
||||
typename simple_queue<T>::size_type push(value_type&& value) {
|
||||
std::unique_lock lock(m_mtx);
|
||||
m_queue.push(std::forward<value_type>(value));
|
||||
auto result = m_queue.size();
|
||||
lock.unlock();
|
||||
m_cv.notify_one();
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Try to get an element from the queue.
|
||||
* @details Returns immediately.
|
||||
* @return An element from the queue, if one is available. \p std::nullopt otherwise
|
||||
*/
|
||||
std::optional<value_type> try_pop() {
|
||||
return pop_impl(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Try to get an element from the queue.
|
||||
* @details Returns as soon as data is availble or after timeout.
|
||||
* @param[in] timeout as <a href="https://en.cppreference.com/w/cpp/chrono/duration">std::chrono::duration</a>.
|
||||
* Smallest unit acceptable is milliseconds.
|
||||
* @return An element from the queue, if one is available. \p std::nullopt otherwise
|
||||
*/
|
||||
template <class Rep, class Period> std::optional<value_type> try_pop(std::chrono::duration<Rep, Period> timeout) {
|
||||
auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(timeout);
|
||||
return pop_impl(ms.count());
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get an element from the queue
|
||||
* @details Only returns, when data is available. Implicitly throws on stop().
|
||||
* @return An element from the queue.
|
||||
*/
|
||||
value_type pop() {
|
||||
return pop_impl(-1).value();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get an element from the queue
|
||||
* @details Only returns, when data is available, or the queue is stopped.
|
||||
* @return An element from the queue. Empty optional if stopped.
|
||||
*/
|
||||
std::optional<value_type> wait_and_pop() {
|
||||
return pop_impl(-1);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Signals that no more items will be pushed and unblocks all waiting consumers.
|
||||
*/
|
||||
void stop() {
|
||||
std::unique_lock lock(m_mtx);
|
||||
m_stop = true;
|
||||
lock.unlock();
|
||||
m_cv.notify_all();
|
||||
}
|
||||
|
||||
private:
|
||||
std::optional<value_type> pop_impl(int timeout_ms) {
|
||||
std::unique_lock lock(m_mtx);
|
||||
auto wait_predicate = [this]() { return not m_queue.empty() or m_stop; };
|
||||
|
||||
if (timeout_ms < 0) {
|
||||
m_cv.wait(lock, wait_predicate);
|
||||
} else if (timeout_ms > 0) {
|
||||
(void)m_cv.wait_for(lock, std::chrono::milliseconds(timeout_ms), wait_predicate);
|
||||
}
|
||||
|
||||
// if the queue is still empty, we return a nullopt. Note that this would be implicitly
|
||||
// handled by simple_queue::pop, but it is added here to be more explcit
|
||||
if (m_queue.empty()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
return m_queue.pop();
|
||||
}
|
||||
|
||||
simple_queue<T> m_queue;
|
||||
std::mutex m_mtx;
|
||||
std::condition_variable m_cv;
|
||||
bool m_stop{false};
|
||||
};
|
||||
|
||||
} // namespace everest::lib::util
|
||||
@@ -0,0 +1,658 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2026 Pionix GmbH and Contributors to EVerest
|
||||
|
||||
/**
|
||||
* @file fixed_vector.hpp
|
||||
* @brief Provides a std::vector-like container with fixed capacity, avoiding dynamic memory allocation.
|
||||
*
|
||||
* The fixed_vector is a sequence container that encapsulates a fixed-size array. It provides an interface
|
||||
* similar to std::vector but does not allocate memory on the heap. Its capacity is determined at compile time
|
||||
* by the template parameter N. This makes it suitable for real-time and embedded applications where
|
||||
* dynamic memory allocation is disallowed or undesirable.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <initializer_list>
|
||||
#include <iterator>
|
||||
#include <memory>
|
||||
#include <stdexcept>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace everest::lib::util {
|
||||
|
||||
/**
|
||||
* @brief A container with std::vector-like interface but with fixed capacity.
|
||||
*
|
||||
* This class mimics the behavior of std::vector but stores its elements in a fixed-size internal buffer,
|
||||
* avoiding heap allocations. The capacity is specified at compile time.
|
||||
* If the number of elements exceeds the capacity, it results in an exception (`std::length_error`).
|
||||
*
|
||||
* @tparam T The type of elements.
|
||||
* @tparam N The maximum number of elements the vector can hold (its capacity).
|
||||
*/
|
||||
template <typename T, std::size_t N> class fixed_vector {
|
||||
static_assert(!std::is_move_constructible_v<T> || std::is_nothrow_move_constructible_v<T>,
|
||||
"fixed_vector requires T's move constructor to be noexcept. "
|
||||
"Types with throwing move constructors are not supported because "
|
||||
"fixed_vector cannot propagate move-construction failures safely.");
|
||||
static_assert(!std::is_move_assignable_v<T> || std::is_nothrow_move_assignable_v<T>,
|
||||
"fixed_vector requires T's move assignment to be noexcept. "
|
||||
"Types with throwing move assignment are not supported because "
|
||||
"fixed_vector cannot propagate move-assignment failures safely.");
|
||||
|
||||
public:
|
||||
//- Member types
|
||||
using value_type = T;
|
||||
using size_type = std::size_t;
|
||||
using difference_type = std::ptrdiff_t;
|
||||
using reference = value_type&;
|
||||
using const_reference = const value_type&;
|
||||
using pointer = value_type*;
|
||||
using const_pointer = const value_type*;
|
||||
using iterator = pointer;
|
||||
using const_iterator = const_pointer;
|
||||
using reverse_iterator = std::reverse_iterator<iterator>;
|
||||
using const_reverse_iterator = std::reverse_iterator<const_iterator>;
|
||||
|
||||
/**
|
||||
* @brief Constructs an empty fixed_vector.
|
||||
*/
|
||||
constexpr fixed_vector() noexcept = default;
|
||||
|
||||
/**
|
||||
* @brief Destroys the fixed_vector, calling destructors for all contained elements.
|
||||
*/
|
||||
~fixed_vector() {
|
||||
clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Copy constructor. Constructs the vector with a copy of the contents of other.
|
||||
* @param other another fixed_vector object to be used as source to initialize the elements of the container with.
|
||||
*/
|
||||
fixed_vector(const fixed_vector& other) {
|
||||
copy_construct_from(other);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Move constructor. Constructs the vector with the contents of other using move semantics.
|
||||
* @details Since T is required to be nothrow move-constructible, this operation is always noexcept.
|
||||
* After the move, `other` is empty.
|
||||
* @param other another fixed_vector object to be used as source.
|
||||
*/
|
||||
fixed_vector(fixed_vector&& other) noexcept {
|
||||
move_construct_from(std::move(other));
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Constructs the vector with the contents of the initializer list.
|
||||
* @param init initializer list to initialize the elements of the container with.
|
||||
* @throws std::length_error if the size of the initializer list is greater than the capacity of the vector.
|
||||
*/
|
||||
fixed_vector(std::initializer_list<T> init) {
|
||||
if (init.size() > N) {
|
||||
throw std::length_error("Initializer list size exceeds fixed_vector capacity");
|
||||
}
|
||||
|
||||
// std::uninitialized_copy constructs elements in-place. If an element's
|
||||
// constructor throws, it destroys any elements already created.
|
||||
// We only update `size_` after all elements are successfully constructed,
|
||||
// which provides the strong exception guarantee.
|
||||
std::uninitialized_copy(init.begin(), init.end(), data());
|
||||
size_ = init.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Constructs the vector with the contents of an std::vector.
|
||||
* @param vec std::vector to initialize the elements of the container with.
|
||||
* @throws std::length_error if the size of the std::vector is greater than the capacity of the fixed_vector.
|
||||
*/
|
||||
explicit fixed_vector(const std::vector<T>& vec) {
|
||||
if (vec.size() > N) {
|
||||
throw std::length_error("std::vector size exceeds fixed_vector capacity");
|
||||
}
|
||||
|
||||
std::uninitialized_copy(vec.begin(), vec.end(), data());
|
||||
size_ = vec.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Copy assignment operator. Replaces the contents with a copy of the contents of other.
|
||||
* @param other another fixed_vector object to be used as source.
|
||||
* @return *this
|
||||
*/
|
||||
fixed_vector& operator=(const fixed_vector& other) {
|
||||
if (this != &other) {
|
||||
copy_assign_from(other);
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Move assignment operator. Replaces the contents with those of other using move semantics.
|
||||
* @details Since T is required to have nothrow move operations, this is always noexcept.
|
||||
* After the move, `other` is empty.
|
||||
* @param other another fixed_vector object to be used as source.
|
||||
* @return *this
|
||||
*/
|
||||
fixed_vector& operator=(fixed_vector&& other) noexcept {
|
||||
if (this != &other) {
|
||||
move_assign_from(std::move(other));
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
// Element access
|
||||
/**
|
||||
* @brief Returns a reference to the element at specified location `pos`, with bounds checking.
|
||||
* @param pos position of the element to return.
|
||||
* @return Reference to the requested element.
|
||||
* @throws std::out_of_range if `pos >= size()`.
|
||||
*/
|
||||
constexpr reference at(size_type pos) {
|
||||
if (pos >= size_) {
|
||||
throw std::out_of_range("fixed_vector::at");
|
||||
}
|
||||
return data()[pos];
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Returns a const reference to the element at specified location `pos`, with bounds checking.
|
||||
* @param pos position of the element to return.
|
||||
* @return Const reference to the requested element.
|
||||
* @throws std::out_of_range if `pos >= size()`.
|
||||
*/
|
||||
constexpr const_reference at(size_type pos) const {
|
||||
if (pos >= size_) {
|
||||
throw std::out_of_range("fixed_vector::at");
|
||||
}
|
||||
return data()[pos];
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Returns a reference to the element at specified location `pos`. No bounds checking is performed.
|
||||
* @param pos position of the element to return.
|
||||
* @return Reference to the element at `pos`.
|
||||
*/
|
||||
constexpr reference operator[](size_type pos) {
|
||||
return data()[pos];
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Returns a const reference to the element at specified location `pos`. No bounds checking is performed.
|
||||
* @param pos position of the element to return.
|
||||
* @return Const reference to the element at `pos`.
|
||||
*/
|
||||
constexpr const_reference operator[](size_type pos) const {
|
||||
return data()[pos];
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Returns a reference to the first element in the container.
|
||||
* @details Calling front on an empty container is undefined.
|
||||
* @return Reference to the first element.
|
||||
*/
|
||||
constexpr reference front() {
|
||||
return data()[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Returns a const reference to the first element in the container.
|
||||
* @details Calling front on an empty container is undefined.
|
||||
* @return Const reference to the first element.
|
||||
*/
|
||||
constexpr const_reference front() const {
|
||||
return data()[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Returns a reference to the last element in the container.
|
||||
* @details Calling back on an empty container is undefined.
|
||||
* @return Reference to the last element.
|
||||
*/
|
||||
constexpr reference back() {
|
||||
return data()[size_ - 1];
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Returns a const reference to the last element in the container.
|
||||
* @details Calling back on an empty container is undefined.
|
||||
* @return Const reference to the last element.
|
||||
*/
|
||||
constexpr const_reference back() const {
|
||||
return data()[size_ - 1];
|
||||
}
|
||||
|
||||
// Iterators
|
||||
/**
|
||||
* @brief Returns an iterator to the first element of the vector.
|
||||
* @return Iterator to the first element.
|
||||
*/
|
||||
constexpr iterator begin() noexcept {
|
||||
return data();
|
||||
}
|
||||
/**
|
||||
* @brief Returns a const iterator to the first element of the vector.
|
||||
* @return Const iterator to the first element.
|
||||
*/
|
||||
constexpr const_iterator begin() const noexcept {
|
||||
return data();
|
||||
}
|
||||
/**
|
||||
* @brief Returns a const iterator to the first element of the vector.
|
||||
* @return Const iterator to the first element.
|
||||
*/
|
||||
constexpr const_iterator cbegin() const noexcept {
|
||||
return data();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Returns an iterator to the element following the last element of the vector.
|
||||
* @return Iterator to the element following the last element.
|
||||
*/
|
||||
constexpr iterator end() noexcept {
|
||||
return data() + size_;
|
||||
}
|
||||
/**
|
||||
* @brief Returns a const iterator to the element following the last element of the vector.
|
||||
* @return Const iterator to the element following the last element.
|
||||
*/
|
||||
constexpr const_iterator end() const noexcept {
|
||||
return data() + size_;
|
||||
}
|
||||
/**
|
||||
* @brief Returns a const iterator to the element following the last element of the vector.
|
||||
* @return Const iterator to the element following the last element.
|
||||
*/
|
||||
constexpr const_iterator cend() const noexcept {
|
||||
return data() + size_;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Returns a reverse iterator to the first element of the reversed vector.
|
||||
* @return Reverse iterator to the first element.
|
||||
*/
|
||||
constexpr reverse_iterator rbegin() noexcept {
|
||||
return reverse_iterator(end());
|
||||
}
|
||||
/**
|
||||
* @brief Returns a const reverse iterator to the first element of the reversed vector.
|
||||
* @return Const reverse iterator to the first element.
|
||||
*/
|
||||
constexpr const_reverse_iterator rbegin() const noexcept {
|
||||
return const_reverse_iterator(end());
|
||||
}
|
||||
/**
|
||||
* @brief Returns a const reverse iterator to the first element of the reversed vector.
|
||||
* @return Const reverse iterator to the first element.
|
||||
*/
|
||||
constexpr const_reverse_iterator crbegin() const noexcept {
|
||||
return const_reverse_iterator(end());
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Returns a reverse iterator to the element following the last element of the reversed vector.
|
||||
* @return Reverse iterator to the element following the last element.
|
||||
*/
|
||||
constexpr reverse_iterator rend() noexcept {
|
||||
return reverse_iterator(begin());
|
||||
}
|
||||
/**
|
||||
* @brief Returns a const reverse iterator to the element following the last element of the reversed vector.
|
||||
* @return Const reverse iterator to the element following the last element.
|
||||
*/
|
||||
constexpr const_reverse_iterator rend() const noexcept {
|
||||
return const_reverse_iterator(begin());
|
||||
}
|
||||
/**
|
||||
* @brief Returns a const reverse iterator to the element following the last element of the reversed vector.
|
||||
* @return Const reverse iterator to the element following the last element.
|
||||
*/
|
||||
constexpr const_reverse_iterator crend() const noexcept {
|
||||
return const_reverse_iterator(begin());
|
||||
}
|
||||
|
||||
// Capacity
|
||||
/**
|
||||
* @brief Checks if the container has no elements.
|
||||
* @return true if the container is empty, false otherwise.
|
||||
*/
|
||||
[[nodiscard]] constexpr bool empty() const noexcept {
|
||||
return size_ == 0;
|
||||
}
|
||||
/**
|
||||
* @brief Returns the number of elements in the container.
|
||||
* @return The number of elements in the container.
|
||||
*/
|
||||
[[nodiscard]] constexpr size_type size() const noexcept {
|
||||
return size_;
|
||||
}
|
||||
/**
|
||||
* @brief Returns the maximum number of elements the container is able to hold.
|
||||
* @return The maximum number of elements.
|
||||
*/
|
||||
[[nodiscard]] constexpr size_type max_size() const noexcept {
|
||||
return N;
|
||||
}
|
||||
/**
|
||||
* @brief Returns the number of elements that the container has currently allocated space for. For fixed_vector,
|
||||
* this is always N.
|
||||
* @return The capacity of the container.
|
||||
*/
|
||||
[[nodiscard]] constexpr size_type capacity() const noexcept {
|
||||
return N;
|
||||
}
|
||||
|
||||
// Modifiers
|
||||
/**
|
||||
* @brief Erases all elements from the container.
|
||||
*/
|
||||
void clear() noexcept {
|
||||
if constexpr (!std::is_trivially_destructible_v<T>) {
|
||||
for (size_type i = 0; i < size_; ++i) {
|
||||
std::destroy_at(data() + i);
|
||||
}
|
||||
}
|
||||
size_ = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Appends a new element to the end of the container, constructed in-place.
|
||||
* @param args arguments to forward to the constructor of the element.
|
||||
* @return Reference to the emplaced element.
|
||||
* @throws std::length_error if the container is full.
|
||||
*/
|
||||
template <class... Args> reference emplace_back(Args&&... args) {
|
||||
if (size_ >= N) {
|
||||
throw std::length_error("fixed_vector is full");
|
||||
}
|
||||
pointer ptr = data() + size_;
|
||||
new (ptr) T(std::forward<Args>(args)...);
|
||||
++size_;
|
||||
return *ptr;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Appends a new element to the end of the container, constructed in-place, without throwing exceptions.
|
||||
* @details If the container is full, or if the constructor of the element throws, this function does nothing
|
||||
* and returns `nullptr`.
|
||||
* @param args arguments to forward to the constructor of the element.
|
||||
* @return A pointer to the new element if successful, or `nullptr` otherwise.
|
||||
*/
|
||||
template <class... Args> pointer try_emplace_back(Args&&... args) noexcept {
|
||||
try {
|
||||
return &emplace_back(std::forward<Args>(args)...);
|
||||
} catch (...) {
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Appends the given element `value` to the end of the container.
|
||||
* @param value the value of the element to append.
|
||||
* @throws std::length_error if the container is full.
|
||||
*/
|
||||
void push_back(const T& value) {
|
||||
emplace_back(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Appends the given element `value` to the end of the container using move semantics.
|
||||
* @param value the value of the element to append.
|
||||
* @throws std::length_error if the container is full.
|
||||
*/
|
||||
void push_back(T&& value) {
|
||||
emplace_back(std::move(value));
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Removes the last element of the container.
|
||||
* @details Calling pop_back on an empty container is a no-op in this implementation.
|
||||
*/
|
||||
void pop_back() {
|
||||
if (size_ > 0) {
|
||||
--size_;
|
||||
if constexpr (!std::is_trivially_destructible_v<T>) {
|
||||
std::destroy_at(data() + size_);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Erases the element at `pos`.
|
||||
* @param pos iterator to the element to remove.
|
||||
* @return Iterator following the last removed element.
|
||||
*/
|
||||
iterator erase(const_iterator pos) {
|
||||
const auto offset = std::distance(cbegin(), pos);
|
||||
iterator ita = begin() + offset;
|
||||
|
||||
std::move(ita + 1, end(), ita);
|
||||
|
||||
pop_back();
|
||||
|
||||
return ita;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Erases the elements in the range `[first, last)`.
|
||||
* @param first the first element to remove.
|
||||
* @param last the last element to remove.
|
||||
* @return Iterator following the last removed element.
|
||||
*/
|
||||
iterator erase(const_iterator first, const_iterator last) {
|
||||
iterator it_first = begin() + std::distance(cbegin(), first);
|
||||
iterator it_last = begin() + std::distance(cbegin(), last);
|
||||
auto count = std::distance(it_first, it_last);
|
||||
|
||||
if (count > 0) {
|
||||
std::move(it_last, end(), it_first);
|
||||
|
||||
auto new_size = size_ - count;
|
||||
if constexpr (!std::is_trivially_destructible_v<T>) {
|
||||
for (size_type i = new_size; i < size_; ++i) {
|
||||
std::destroy_at(data() + i);
|
||||
}
|
||||
}
|
||||
size_ = new_size;
|
||||
}
|
||||
|
||||
return it_first;
|
||||
}
|
||||
|
||||
private:
|
||||
alignas(T) std::array<std::byte, N * sizeof(T)> storage_; ///< Internal storage for elements.
|
||||
size_type size_{0}; ///< Current number of elements.
|
||||
|
||||
// --- Implementation helpers for special members ---
|
||||
|
||||
// Copy-construct
|
||||
/**
|
||||
* @brief Helper for copy construction: constructs this fixed_vector by copy-constructing elements from another.
|
||||
* @details This overload is enabled if `T` is copy-constructible.
|
||||
* @param other The fixed_vector to copy elements from.
|
||||
*/
|
||||
template <typename U = T, std::enable_if_t<std::is_copy_constructible_v<U>>* = nullptr>
|
||||
void copy_construct_from(const fixed_vector& other) {
|
||||
for (const auto& elem : other) {
|
||||
push_back(elem);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Helper for copy construction: provides a compile-time error if `T` is not copy-constructible.
|
||||
* @details This overload is enabled if `T` is not copy-constructible, triggering a `static_assert`.
|
||||
* @param other Unused, present for signature matching.
|
||||
*/
|
||||
template <typename U = T, std::enable_if_t<!std::is_copy_constructible_v<U>>* = nullptr>
|
||||
void copy_construct_from(const fixed_vector& /*other*/) {
|
||||
static_assert(std::is_copy_constructible_v<U>,
|
||||
"fixed_vector requires T to be copy-constructible for copy construction.");
|
||||
}
|
||||
|
||||
// Copy-assign
|
||||
/**
|
||||
* @brief Helper for copy assignment: assigns elements from another fixed_vector.
|
||||
* @details This overload is enabled if `T` is both copy-constructible and copy-assignable.
|
||||
* It uses an efficient element-wise assignment strategy.
|
||||
* @param other The fixed_vector to assign elements from.
|
||||
* @return A reference to this fixed_vector.
|
||||
*/
|
||||
template <typename U = T,
|
||||
std::enable_if_t<std::is_copy_constructible_v<U> && std::is_copy_assignable_v<U>>* = nullptr>
|
||||
fixed_vector& copy_assign_from(const fixed_vector& other) {
|
||||
const size_type copy_len = std::min(size_, other.size_);
|
||||
std::copy(other.begin(), other.begin() + copy_len, begin());
|
||||
|
||||
if (size_ < other.size_) {
|
||||
for (size_type i = size_; i < other.size_; ++i) {
|
||||
push_back(other[i]);
|
||||
}
|
||||
} else if (size_ > other.size_) {
|
||||
const size_type old_size = size_;
|
||||
size_ = other.size_;
|
||||
if constexpr (!std::is_trivially_destructible_v<T>) {
|
||||
for (size_type i = size_; i < old_size; ++i) {
|
||||
std::destroy_at(data() + i);
|
||||
}
|
||||
}
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Helper for copy assignment: provides a compile-time error if `T` is not copy-constructible or not
|
||||
* copy-assignable.
|
||||
* @details This overload is enabled if `T` does not meet the requirements, triggering `static_assert`s.
|
||||
* @param other Unused, present for signature matching.
|
||||
* @return A reference to this fixed_vector (never reached due to static_assert).
|
||||
*/
|
||||
template <typename U = T,
|
||||
std::enable_if_t<!(std::is_copy_constructible_v<U> && std::is_copy_assignable_v<U>)>* = nullptr>
|
||||
fixed_vector& copy_assign_from(const fixed_vector& /*other*/) {
|
||||
static_assert(std::is_copy_constructible_v<U>,
|
||||
"fixed_vector requires T to be copy-constructible for copy assignment.");
|
||||
static_assert(std::is_copy_assignable_v<U>,
|
||||
"fixed_vector requires T to be copy-assignable for copy assignment.");
|
||||
return *this;
|
||||
}
|
||||
|
||||
// Move-construct
|
||||
/**
|
||||
* @brief Helper for move construction.
|
||||
* @details This overload is enabled if `T` is nothrow move-constructible (enforced by class-level
|
||||
* static_assert). It is `noexcept`.
|
||||
*/
|
||||
template <typename U = T, std::enable_if_t<std::is_nothrow_move_constructible_v<U>>* = nullptr>
|
||||
void move_construct_from(fixed_vector&& other) noexcept {
|
||||
for (auto& elem : other) {
|
||||
if (try_emplace_back(std::move(elem)) == nullptr) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
other.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Helper for move construction: provides a compile-time error if `T` is not move-constructible.
|
||||
* @details This overload is enabled if `T` is not move-constructible, triggering a `static_assert`.
|
||||
* @param other Unused, present for signature matching.
|
||||
*/
|
||||
template <typename U = T, std::enable_if_t<!std::is_nothrow_move_constructible_v<U>>* = nullptr>
|
||||
void move_construct_from(fixed_vector&& /*other*/) {
|
||||
static_assert(std::is_nothrow_move_constructible_v<U>,
|
||||
"fixed_vector requires T to be nothrow move-constructible for move construction.");
|
||||
}
|
||||
|
||||
// Move-assign
|
||||
/**
|
||||
* @brief Helper for move assignment.
|
||||
* @details This overload is enabled if `T`'s move operations are nothrow (enforced by class-level
|
||||
* static_assert). It is `noexcept`.
|
||||
*/
|
||||
template <typename U = T, std::enable_if_t<(std::is_nothrow_move_assignable_v<U> &&
|
||||
std::is_nothrow_move_constructible_v<U>)>* = nullptr>
|
||||
void move_assign_from(fixed_vector&& other) noexcept {
|
||||
const size_type move_len = std::min(size_, other.size_);
|
||||
std::move(other.begin(), other.begin() + move_len, begin());
|
||||
|
||||
if (size_ < other.size_) {
|
||||
for (size_type i = size_; i < other.size_; ++i) {
|
||||
if (try_emplace_back(std::move(other[i])) == nullptr) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else if (size_ > other.size_) {
|
||||
const size_type old_size = size_;
|
||||
size_ = other.size_;
|
||||
if constexpr (!std::is_trivially_destructible_v<T>) {
|
||||
for (size_type i = size_; i < old_size; ++i) {
|
||||
std::destroy_at(data() + i);
|
||||
}
|
||||
}
|
||||
}
|
||||
other.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Helper for move assignment: provides a compile-time error if `T` does not meet the requirements.
|
||||
* @details This overload is enabled if `T`'s move operations are not nothrow, triggering `static_assert`s.
|
||||
* @param other Unused, present for signature matching.
|
||||
*/
|
||||
template <typename U = T, std::enable_if_t<!(std::is_nothrow_move_assignable_v<U> &&
|
||||
std::is_nothrow_move_constructible_v<U>)>* = nullptr>
|
||||
void move_assign_from(fixed_vector&& /*other*/) {
|
||||
static_assert(std::is_nothrow_move_constructible_v<U>,
|
||||
"fixed_vector requires T to be nothrow move-constructible for move assignment.");
|
||||
static_assert(std::is_nothrow_move_assignable_v<U>,
|
||||
"fixed_vector requires T to be nothrow move-assignable for move assignment.");
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Returns a pointer to the underlying storage.
|
||||
*/
|
||||
constexpr pointer data() noexcept {
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast): needed for appropriate type
|
||||
return reinterpret_cast<pointer>(storage_.data());
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Returns a const pointer to the underlying storage.
|
||||
*/
|
||||
constexpr const_pointer data() const noexcept {
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast): needed for appropriate type
|
||||
return reinterpret_cast<const_pointer>(storage_.data());
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Checks if the contents of two fixed_vectors are equal.
|
||||
* @relates fixed_vector
|
||||
* @param lhs The left-hand side vector.
|
||||
* @param rhs The right-hand side vector.
|
||||
* @return true if the contents are equal, false otherwise.
|
||||
*/
|
||||
template <typename T, std::size_t N> bool operator==(const fixed_vector<T, N>& lhs, const fixed_vector<T, N>& rhs) {
|
||||
if (lhs.size() != rhs.size()) {
|
||||
return false;
|
||||
}
|
||||
return std::equal(lhs.begin(), lhs.end(), rhs.begin());
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Checks if the contents of two fixed_vectors are not equal.
|
||||
* @relates fixed_vector
|
||||
* @param lhs The left-hand side vector.
|
||||
* @param rhs The right-hand side vector.
|
||||
* @return true if the contents are not equal, false otherwise.
|
||||
*/
|
||||
template <typename T, std::size_t N> bool operator!=(const fixed_vector<T, N>& lhs, const fixed_vector<T, N>& rhs) {
|
||||
return !(lhs == rhs);
|
||||
}
|
||||
|
||||
} // namespace everest::lib::util
|
||||
Reference in New Issue
Block a user