Просмотр исходного кода

process/scheduler: refine API (finally), add allocator support - see #1257

skypjack 7 месяцев назад
Родитель
Сommit
5c31fda9bd

+ 2 - 3
TODO

@@ -38,6 +38,5 @@ TODO:
 * avoid copying meta_type/data/func nodes
 * paged vector as a standalone class
 * operator bool to meta custom
-* process: doc
-* process_adaptor: write more tests for plain funcs and the like
-* test shared_process_from
+* process: doc and natvis
+* test process and scheduler with allocators

+ 1 - 4
src/entt/process/fwd.hpp

@@ -6,12 +6,9 @@
 
 namespace entt {
 
-template<typename>
+template<typename, typename = std::allocator<void>>
 class basic_process;
 
-template<typename, typename>
-struct process_adaptor;
-
 /*! @brief Alias declaration for the most common use case. */
 using process = basic_process<std::uint32_t>;
 

+ 76 - 96
src/entt/process/process.hpp

@@ -5,11 +5,21 @@
 #include <memory>
 #include <type_traits>
 #include <utility>
+#include "../core/compressed_pair.hpp"
 #include "../core/type_traits.hpp"
 #include "fwd.hpp"
 
 namespace entt {
 
+/*! @cond TURN_OFF_DOXYGEN */
+namespace internal {
+
+template<typename, typename, typename>
+struct process_adaptor;
+
+} // namespace internal
+/*! @endcond */
+
 /**
  * @brief Base class for processes.
  *
@@ -56,8 +66,9 @@ namespace entt {
  * @sa scheduler
  *
  * @tparam Delta Type to use to provide elapsed time.
+ * @tparam Allocator Type of allocator used to manage memory and elements.
  */
-template<typename Delta>
+template<typename Delta, typename Allocator>
 class basic_process {
     enum class state : std::uint8_t {
         idle = 0,
@@ -79,34 +90,51 @@ class basic_process {
     virtual void aborted() {}
 
 public:
+    /*! @brief Allocator type. */
+    using allocator_type = Allocator;
     /*! @brief Type used to provide elapsed time. */
     using delta_type = Delta;
 
     /*! @brief Default constructor. */
-    constexpr basic_process()
-        : next{},
+    basic_process()
+        : basic_process{allocator_type{}} {}
+
+    /**
+     * @brief Constructs a scheduler with a given allocator.
+     * @param allocator The allocator to use.
+     */
+    explicit basic_process(const allocator_type &allocator)
+        : next{nullptr, allocator},
           current{state::idle} {}
 
+    /*! @brief Default copy constructor, deleted on purpose. */
+    basic_process(const basic_process &) = delete;
+
+    /*! @brief Default move constructor, deleted on purpose. */
+    basic_process(basic_process &&) = delete;
+
     /*! @brief Default destructor. */
     virtual ~basic_process() = default;
 
-    /*! @brief Default copy constructor. */
-    basic_process(const basic_process &) = default;
-
-    /*! @brief Default move constructor. */
-    basic_process(basic_process &&) noexcept = default;
+    /**
+     * @brief Default copy assignment operator, deleted on purpose.
+     * @return This process scheduler.
+     */
+    basic_process &operator=(const basic_process &) = delete;
 
     /**
-     * @brief Default copy assignment operator.
-     * @return This process.
+     * @brief Default move assignment operator, deleted on purpose.
+     * @return This process scheduler.
      */
-    basic_process &operator=(const basic_process &) = default;
+    basic_process &operator=(basic_process &&) = delete;
 
     /**
-     * @brief Default move assignment operator.
-     * @return This process.
+     * @brief Returns the associated allocator.
+     * @return The associated allocator.
      */
-    basic_process &operator=(basic_process &&) noexcept = default;
+    [[nodiscard]] constexpr allocator_type get_allocator() const noexcept {
+        return next.second();
+    }
 
     /*! @brief Aborts a process if it's still alive, otherwise does nothing. */
     void abort() {
@@ -183,12 +211,28 @@ public:
 
     /**
      * @brief Assigns a child process to run in case of success.
-     * @param child A child process to run in case of success.
-     * @return A reference to the newly assigned child process.
+     * @tparam Type Type of child process to create.
+     * @tparam Args Types of arguments to use to initialize the child process.
+     * @param args Parameters to use to initialize the child process.
+     * @return A reference to the newly created child process.
+     */
+    template<typename Type, typename... Args>
+    basic_process &then(Args &&...args) {
+        const auto &allocator = next.second();
+        return *(next.first() = std::allocate_shared<Type>(allocator, allocator, std::forward<Args>(args)...));
+    }
+
+    /**
+     * @brief Assigns a child process to run in case of success.
+     * @tparam Func Type of child process to create.
+     * @param func Either a lambda or a functor to use as a child process.
+     * @return A reference to the newly created child process.
      */
-    basic_process &then(std::shared_ptr<basic_process> child) {
-        ENTT_ASSERT(child, "Null process not allowed");
-        return *(next = std::move(child));
+    template<typename Func>
+    basic_process &then(Func func) {
+        const auto &allocator = next.second();
+        using process_type = internal::process_adaptor<delta_type, Func, allocator_type>;
+        return *(next.first() = std::allocate_shared<process_type>(allocator, allocator, std::move(func)));
     }
 
     /**
@@ -196,7 +240,7 @@ public:
      * @return The child process attached to the object, if any.
      */
     std::shared_ptr<basic_process> peek() {
-        return next;
+        return next.first();
     }
 
     /**
@@ -237,57 +281,23 @@ public:
     }
 
 private:
-    std::shared_ptr<basic_process> next;
+    compressed_pair<std::shared_ptr<basic_process>, allocator_type> next;
     state current;
 };
 
-/**
- * @brief Adaptor for lambdas and functors to turn them into processes.
- *
- * Lambdas and functors can't be used directly with a scheduler for they are not
- * properly defined processes with managed life cycles.<br/>
- * This class helps in filling the gap and turning lambdas and functors into
- * full featured processes usable by a scheduler.
- *
- * The signature of the function call operator should be equivalent to the
- * following:
- *
- * @code{.cpp}
- * void(basic_process<Delta> &process, Delta delta, void *data);
- * @endcode
- *
- * Where:
- *
- * * `process` is a reference to the handler on the library side.
- * * `delta` is the elapsed time.
- * * `data` is an opaque pointer to user data if any, `nullptr` otherwise.
- *
- * @sa process
- * @sa scheduler
- *
- * @tparam Delta Type to use to provide elapsed time.
- * @tparam Func Actual type of process.
- */
-template<typename Delta, typename Func>
-struct process_adaptor: public basic_process<Delta> {
-    /*! @brief Base type. */
-    using base_type = basic_process<Delta>;
-    /*! @brief Type used to provide elapsed time. */
+/*! @cond TURN_OFF_DOXYGEN */
+namespace internal {
+
+template<typename Delta, typename Func, typename Allocator>
+struct process_adaptor: public basic_process<Delta, Allocator> {
+    using allocator_type = Allocator;
+    using base_type = basic_process<Delta, Allocator>;
     using delta_type = typename base_type::delta_type;
 
-    /**
-     * @brief Constructs a process adaptor from a lambda or a functor.
-     * @param proc Actual process to use under the hood.
-     */
-    process_adaptor(Func proc)
-        : base_type{},
+    process_adaptor(const allocator_type &allocator, Func proc)
+        : base_type{allocator},
           func{std::move(proc)} {}
 
-    /**
-     * @brief Updates a process and its internal state if required.
-     * @param delta Elapsed time.
-     * @param data Optional data.
-     */
     void update(const delta_type delta, void *data) override {
         func(*this, delta, data);
     }
@@ -296,38 +306,8 @@ private:
     Func func;
 };
 
-/**
- * @brief Deduction guide.
- * @tparam Func Actual type of process.
- */
-template<typename Func>
-process_adaptor(Func) -> process_adaptor<nth_argument_t<1u, Func>, Func>;
-
-/**
- * @brief Utility function to create shared processes from lambdas or functors.
- * @tparam Func Actual type of process.
- * @param func Actual process to use under the hood.
- * @return A properly initialized shared process.
- */
-template<typename Func>
-auto process_from(Func func) {
-    using type = process_adaptor<nth_argument_t<1u, Func>, Func>;
-    return std::make_shared<type>(std::move(func));
-}
-
-/**
- * @brief Utility function to create shared processes from lambdas or functors.
- * @tparam Allocator Type of allocator used to manage memory and elements.
- * @tparam Func Actual type of process.
- * @param allocator The allocator to use.
- * @param func Actual process to use under the hood.
- * @return A properly initialized shared process.
- */
-template<typename Allocator, typename Func>
-auto process_from(const Allocator &allocator, Func func) {
-    using type = process_adaptor<nth_argument_t<1u, Func>, Func>;
-    return std::allocate_shared<type>(allocator, std::move(func));
-}
+} // namespace internal
+/*! @endcond */
 
 } // namespace entt
 

+ 22 - 6
src/entt/process/scheduler.hpp

@@ -34,7 +34,7 @@ namespace entt {
  */
 template<typename Delta, typename Allocator>
 class basic_scheduler {
-    using base_type = basic_process<Delta>;
+    using base_type = basic_process<Delta, Allocator>;
     using alloc_traits = std::allocator_traits<Allocator>;
     using container_allocator = typename alloc_traits::template rebind_alloc<std::shared_ptr<base_type>>;
     using container_type = std::vector<std::shared_ptr<base_type>, container_allocator>;
@@ -145,12 +145,28 @@ public:
 
     /**
      * @brief Schedules a process for the next tick.
-     * @param proc a process to schedule.
-     * @return A reference to the newly assigned process.
+     * @tparam Type Type of process to create.
+     * @tparam Args Types of arguments to use to initialize the process.
+     * @param args Parameters to use to initialize the process.
+     * @return A reference to the newly created process.
      */
-    type &attach(std::shared_ptr<type> proc) {
-        ENTT_ASSERT(proc, "Null process not allowed");
-        return *handlers.first().emplace_back(std::move(proc));
+    template<typename Type, typename... Args>
+    type &attach(Args &&...args) {
+        const auto &allocator = handlers.second();
+        return *handlers.first().emplace_back(std::allocate_shared<Type>(allocator, allocator, std::forward<Args>(args)...));
+    }
+
+    /**
+     * @brief Schedules a process for the next tick.
+     * @tparam Func Type of process to create.
+     * @param func Either a lambda or a functor to use as a process.
+     * @return A reference to the newly created process.
+     */
+    template<typename Func>
+    type &attach(Func func) {
+        const auto &allocator = handlers.second();
+        using process_type = internal::process_adaptor<delta_type, Func, allocator_type>;
+        return *handlers.first().emplace_back(std::allocate_shared<process_type>(allocator, allocator, std::move(func)));
     }
 
     /**

+ 7 - 62
test/entt/process/process.cpp

@@ -187,70 +187,15 @@ TEST(Process, AbortImmediately) {
 }
 
 TEST(Process, ThenPeek) {
-    auto process = std::make_shared<test_process<int>>();
+    test_process<int> process{};
 
-    ASSERT_FALSE(process->peek());
+    ASSERT_FALSE(process.peek());
 
-    process->then(std::make_shared<test_process<int>>()).then(std::make_shared<test_process<int>>());
+    process.then<test_process<int>>().then<test_process<int>>();
 
-    ASSERT_TRUE(process->peek());
-    ASSERT_TRUE(process->peek()->peek());
-    ASSERT_FALSE(process->peek()->peek()->peek());
+    ASSERT_TRUE(process.peek());
+    ASSERT_TRUE(process.peek()->peek());
+    ASSERT_FALSE(process.peek()->peek()->peek());
     // peek does not release ownership
-    ASSERT_TRUE(process->peek());
-}
-
-ENTT_DEBUG_TEST(ProcessDeathTest, Then) {
-    auto process = std::make_shared<test_process<int>>();
-
-    ASSERT_DEATH(process->then(nullptr), "");
-}
-
-TEST(ProcessAdaptor, Resolved) {
-    bool updated = false;
-    auto lambda = [&updated](entt::process &proc, std::uint32_t, void *) {
-        ASSERT_FALSE(updated);
-        updated = true;
-        proc.succeed();
-    };
-
-    entt::process_adaptor process{lambda};
-
-    process.tick(0u);
-    process.tick(0u);
-
-    ASSERT_TRUE(process.finished());
-    ASSERT_TRUE(updated);
-}
-
-TEST(ProcessAdaptor, Rejected) {
-    bool updated = false;
-    auto lambda = [&updated](entt::process &proc, std::uint32_t, void *) {
-        ASSERT_FALSE(updated);
-        updated = true;
-        proc.fail();
-    };
-
-    entt::process_adaptor process{lambda};
-
-    process.tick(0u);
-    process.tick(0u);
-
-    ASSERT_TRUE(process.rejected());
-    ASSERT_TRUE(updated);
-}
-
-TEST(ProcessAdaptor, Data) {
-    int value = 0;
-    auto lambda = [](entt::process &proc, std::uint32_t, void *data) {
-        *static_cast<int *>(data) = 2;
-        proc.succeed();
-    };
-
-    entt::process_adaptor process{lambda};
-
-    process.tick(0u, &value);
-
-    ASSERT_TRUE(process.finished());
-    ASSERT_EQ(value, 2);
+    ASSERT_TRUE(process.peek());
 }

+ 31 - 38
test/entt/process/scheduler.cpp

@@ -16,8 +16,10 @@ class foo_process: public entt::process {
     }
 
 public:
-    foo_process(std::function<void()> upd, std::function<void()> abort)
-        : entt::process{},
+    using allocator_type = typename entt::process::allocator_type;
+
+    foo_process(const allocator_type &allocator, std::function<void()> upd, std::function<void()> abort)
+        : entt::process{allocator},
           on_update{std::move(upd)},
           on_aborted{std::move(abort)} {}
 
@@ -58,7 +60,7 @@ TEST(Scheduler, Functionalities) {
     ASSERT_EQ(scheduler.size(), 0u);
     ASSERT_TRUE(scheduler.empty());
 
-    scheduler.attach(std::make_shared<foo_process>([&updated]() { updated = true; }, [&aborted]() { aborted = true; }));
+    scheduler.attach<foo_process>([&updated]() { updated = true; }, [&aborted]() { aborted = true; });
 
     ASSERT_NE(scheduler.size(), 0u);
     ASSERT_FALSE(scheduler.empty());
@@ -83,7 +85,7 @@ TEST(Scheduler, Swap) {
     entt::scheduler other{};
     int counter{};
 
-    scheduler.attach(entt::process_from([&counter](entt::process &, std::uint32_t, void *) { ++counter; }));
+    scheduler.attach([&counter](entt::process &, std::uint32_t, void *) { ++counter; });
 
     ASSERT_EQ(scheduler.size(), 1u);
     ASSERT_EQ(other.size(), 0u);
@@ -110,19 +112,19 @@ TEST(Scheduler, AttachThen) {
     std::pair<int, int> counter{};
 
     // failing process with successor
-    scheduler.attach(std::make_shared<succeeded_process>())
-        .then(std::make_shared<succeeded_process>())
-        .then(std::make_shared<failed_process>())
-        .then(std::make_shared<succeeded_process>());
+    scheduler.attach<succeeded_process>()
+        .then<succeeded_process>()
+        .then<failed_process>()
+        .then<succeeded_process>();
 
     // failing process without successor
-    scheduler.attach(std::make_shared<succeeded_process>())
-        .then(std::make_shared<succeeded_process>())
-        .then(std::make_shared<failed_process>());
+    scheduler.attach<succeeded_process>()
+        .then<succeeded_process>()
+        .then<failed_process>();
 
     // non-failing process
-    scheduler.attach(std::make_shared<succeeded_process>())
-        .then(std::make_shared<succeeded_process>());
+    scheduler.attach<succeeded_process>()
+        .then<succeeded_process>();
 
     while(!scheduler.empty()) {
         scheduler.update(0, &counter);
@@ -132,33 +134,24 @@ TEST(Scheduler, AttachThen) {
     ASSERT_EQ(counter.second, 2u);
 }
 
-ENTT_DEBUG_TEST(SchedulerDeathTest, Attach) {
-    entt::scheduler scheduler{};
-
-    ASSERT_DEATH(scheduler.attach(nullptr), "");
-}
-
 TEST(Scheduler, Functor) {
     entt::scheduler scheduler{};
 
     bool first_functor = false;
     bool second_functor = false;
 
-    auto attach = [&first_functor](entt::process &proc, std::uint32_t, void *) {
-        ASSERT_FALSE(first_functor);
-        first_functor = true;
-        proc.succeed();
-    };
-
-    auto then = [&second_functor](entt::process &proc, std::uint32_t, void *) {
-        ASSERT_FALSE(second_functor);
-        second_functor = true;
-        proc.fail();
-    };
-
-    scheduler.attach(entt::process_from(std::move(attach)))
-        .then(entt::process_from(std::move(then)))
-        .then(entt::process_from([](entt::process &, std::uint32_t, void *) { FAIL(); }));
+    scheduler
+        .attach([&first_functor](entt::process &proc, std::uint32_t, void *) {
+            ASSERT_FALSE(first_functor);
+            first_functor = true;
+            proc.succeed();
+        })
+        .then([&second_functor](entt::process &proc, std::uint32_t, void *) {
+            ASSERT_FALSE(second_functor);
+            second_functor = true;
+            proc.fail();
+        })
+        .then([](entt::process &, std::uint32_t, void *) { FAIL(); });
 
     while(!scheduler.empty()) {
         scheduler.update(0);
@@ -173,10 +166,10 @@ TEST(Scheduler, SpawningProcess) {
     entt::scheduler scheduler{};
     std::pair<int, int> counter{};
 
-    scheduler.attach(entt::process_from([&scheduler](entt::process &proc, std::uint32_t, void *) {
-        scheduler.attach(std::make_shared<succeeded_process>()).then(std::make_shared<failed_process>());
+    scheduler.attach([&scheduler](entt::process &proc, std::uint32_t, void *) {
+        scheduler.attach<succeeded_process>().then<failed_process>();
         proc.succeed();
-    }));
+    });
 
     while(!scheduler.empty()) {
         scheduler.update(0, &counter);
@@ -193,7 +186,7 @@ TEST(Scheduler, CustomAllocator) {
     ASSERT_EQ(scheduler.get_allocator(), allocator);
     ASSERT_FALSE(scheduler.get_allocator() != allocator);
 
-    scheduler.attach(entt::process_from([](entt::process &, std::uint32_t, void *) {}));
+    scheduler.attach([](entt::process &, std::uint32_t, void *) {});
     const decltype(scheduler) other{std::move(scheduler), allocator};
 
     ASSERT_EQ(other.size(), 1u);