diff --git a/core/CMakeLists.txt b/core/CMakeLists.txt index 66246ba1..d9fa3f42 100644 --- a/core/CMakeLists.txt +++ b/core/CMakeLists.txt @@ -21,6 +21,7 @@ set(PUBLIC_HEADERS include/join/condition.hpp include/join/semaphore.hpp include/join/futex.hpp + include/join/function.hpp include/join/thread.hpp include/join/threadpool.hpp include/join/macaddress.hpp diff --git a/core/include/join/function.hpp b/core/include/join/function.hpp new file mode 100644 index 00000000..b131aec4 --- /dev/null +++ b/core/include/join/function.hpp @@ -0,0 +1,247 @@ +/** + * MIT License + * + * Copyright (c) 2026 Mathieu Rabine + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#ifndef JOIN_CORE_FUNCTION_HPP +#define JOIN_CORE_FUNCTION_HPP + +// C++. +#include +#include +#include +#include + +// C. +#include + +namespace join +{ + /** + * @brief fixed-capacity move-only allocation-free alternative to std::function. + */ + template + class Function; + + /** + * @brief fixed-capacity move-only allocation-free alternative to std::function. + */ + template + class Function + { + public: + /** + * @brief default constructor. + */ + Function () noexcept = default; + + /** + * @brief copy constructor. + * @param other other object to copy. + */ + Function (const Function& other) = delete; + + /** + * @brief copy assignment operator. + * @param other other object to copy. + * @return this. + */ + Function& operator= (const Function& other) = delete; + + /** + * @brief move constructor. + * @param other object to move from. + */ + Function (Function&& other) noexcept + { + moveFrom (std::move (other)); + } + + /** + * @brief move assignment operator. + * @param other object to move from. + * @return *this. + */ + Function& operator= (Function&& other) noexcept + { + clear (); + moveFrom (std::move (other)); + return *this; + } + + /** + * @brief construct from nullptr. + */ + Function (std::nullptr_t) noexcept + : Function () + { + } + + /** + * @brief assign nullptr. + */ + Function& operator= (std::nullptr_t) noexcept + { + clear (); + return *this; + } + + /** + * @brief construct with a callable object. + * @param callable callable object. + */ + template < + typename Func, typename DecayedFunc = std::decay_t, + typename = std::enable_if_t::value && + (std::is_void::value || + std::is_convertible, Return>::value)>> + Function (Func&& callable) + { + static_assert (sizeof (DecayedFunc) <= Capacity, "Callable size exceeds Function capacity."); + static_assert (alignof (DecayedFunc) <= Alignment, "Callable alignment exceeds Function alignment."); + static_assert (std::is_nothrow_move_constructible::value, + "Callable must be nothrow move constructible."); + + new (&_storage) DecayedFunc (std::forward (callable)); + + _invoker = [] (void* storage, Args&&... args) -> Return { + return static_cast ((*static_cast (storage)) (std::forward (args)...)); + }; + + _manager = [] (void* dst, void* src) noexcept { + DecayedFunc* source = static_cast (src); + new (dst) DecayedFunc (std::move (*source)); + source->~DecayedFunc (); + }; + + _destructor = [] (void* storage) noexcept { + static_cast (storage)->~DecayedFunc (); + }; + } + + /** + * @brief destructor. + */ + ~Function () + { + clear (); + } + + /** + * @brief invoke the stored callable. + * @param args arguments forwarded to the callable. + * @return result of the invocation. + * @throw std::bad_function_call if no callable is stored. + */ + Return operator() (Args... args) + { + if (!_invoker) + { + throw std::bad_function_call (); + } + return _invoker (&_storage, std::forward (args)...); + } + + /** + * @brief checks whether a callable is stored. + * @return true if a callable is present. + */ + explicit operator bool () const noexcept + { + return _invoker != nullptr; + } + + /** + * @brief clear the stored callable. + */ + void reset () noexcept + { + clear (); + } + + /** + * @brief swap two Function instances. + */ + void swap (Function& other) noexcept + { + Function tmp (std::move (other)); + other = std::move (*this); + *this = std::move (tmp); + } + + private: + /// invocation function pointer type. + using InvokerFunc = Return (*) (void*, Args&&...); + + /// relocation function pointer type. + using ManagerFunc = void (*) (void*, void*); + + /// destructor function pointer type. + using DestructorFunc = void (*) (void*); + + /** + * @brief destroy the stored callable and reset all pointers. + */ + void clear () noexcept + { + if (_destructor) + { + _destructor (&_storage); + _invoker = nullptr; + _manager = nullptr; + _destructor = nullptr; + } + } + + /** + * @brief move all state from another instance (assumes *this is empty). + */ + void moveFrom (Function&& other) noexcept + { + _invoker = other._invoker; + _manager = other._manager; + _destructor = other._destructor; + + if (_manager) + { + _manager (&_storage, &other._storage); + other._invoker = nullptr; + other._manager = nullptr; + other._destructor = nullptr; + } + } + + /// fixed-capacity aligned storage for the callable target. + alignas (Alignment) unsigned char _storage[Capacity]; + + /// pointer to the static invocation wrapper. + InvokerFunc _invoker = nullptr; + + /// pointer to the static move-relocate wrapper. + ManagerFunc _manager = nullptr; + + /// pointer to the static destructor wrapper. + DestructorFunc _destructor = nullptr; + }; +} + +#endif diff --git a/core/tests/CMakeLists.txt b/core/tests/CMakeLists.txt index a3119a13..60553412 100644 --- a/core/tests/CMakeLists.txt +++ b/core/tests/CMakeLists.txt @@ -157,6 +157,11 @@ target_link_libraries(sharedfutex.gtest ${JOIN_CORE} GTest::gtest_main) add_test(NAME sharedfutex.gtest COMMAND sharedfutex.gtest) install(TARGETS sharedfutex.gtest RUNTIME DESTINATION ${CMAKE_INSTALL_DATADIR}/${PROJECT_NAME}/test) +add_executable(function.gtest function_test.cpp) +target_link_libraries(function.gtest ${JOIN_CORE} GTest::gtest_main) +add_test(NAME function.gtest COMMAND function.gtest) +install(TARGETS function.gtest RUNTIME DESTINATION ${CMAKE_INSTALL_DATADIR}/${PROJECT_NAME}/test) + add_executable(thread.gtest thread_test.cpp) target_link_libraries(thread.gtest ${JOIN_CORE} GTest::gtest_main) add_test(NAME thread.gtest COMMAND thread.gtest) diff --git a/core/tests/function_test.cpp b/core/tests/function_test.cpp new file mode 100644 index 00000000..ec4f49f1 --- /dev/null +++ b/core/tests/function_test.cpp @@ -0,0 +1,365 @@ +/** + * MIT License + * + * Copyright (c) 2026 Mathieu Rabine + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +// libjoin. +#include + +// Libraries. +#include + +// C++. +#include +#include + +using join::Function; + +static int add (int a, int b) +{ + return a + b; +} + +struct Multiplier +{ + int factor; + + int operator() (int val) const + { + return val * factor; + } +}; + +struct DestructGuard +{ + bool* flag; + + explicit DestructGuard (bool* f) noexcept + : flag (f) + { + } + + DestructGuard (const DestructGuard&) = delete; + + DestructGuard (DestructGuard&& other) noexcept + : flag (other.flag) + { + other.flag = nullptr; + } + + ~DestructGuard () + { + if (flag) + { + *flag = true; + } + } + + void operator() () const noexcept + { + } +}; + +/** + * @brief Test construction. + */ +TEST (Function, construct) +{ + Function f1; + EXPECT_FALSE (f1); + EXPECT_THROW (f1 (), std::bad_function_call); + + Function f2 (nullptr); + EXPECT_FALSE (f2); + EXPECT_THROW (f2 (), std::bad_function_call); + + Function f3 (add); + ASSERT_TRUE (f3); + EXPECT_EQ (f3 (10, 32), 42); + + Function f4 ([] (const std::string& s) { + return s + "World"; + }); + ASSERT_TRUE (f4); + EXPECT_EQ (f4 ("Hello "), "Hello World"); + + Function f5 (Multiplier{3}); + ASSERT_TRUE (f5); + EXPECT_EQ (f5 (4), 12); +} + +/** + * @brief Test assignment. + */ +TEST (Function, assign) +{ + int counter = 0; + + Function f1 ([&counter] () { + counter = 1; + }); + f1 (); + EXPECT_EQ (counter, 1); + + f1 = [&counter] () { + counter = 42; + }; + f1 (); + EXPECT_EQ (counter, 42); + + Function f2 ([] { + return 42; + }); + ASSERT_TRUE (f2); + + f2 = nullptr; + EXPECT_FALSE (f2); + EXPECT_THROW (f2 (), std::bad_function_call); + + Function f3 ([] { + return 1; + }); + Function f4 ([] { + return 2; + }); + + f4 = std::move (f3); + + EXPECT_FALSE (f3); + ASSERT_TRUE (f4); + EXPECT_EQ (f4 (), 1); +} + +/** + * @brief Test move. + */ +TEST (Function, move) +{ + auto ptr = std::make_unique (100); + + Function f1 ([p = std::move (ptr)] () { + return *p + 50; + }); + ASSERT_TRUE (f1); + + Function f2 (std::move (f1)); + EXPECT_FALSE (f1); + ASSERT_TRUE (f2); + EXPECT_EQ (f2 (), 150); +} + +/** + * @brief Test non-trivial move. + */ +TEST (Function, nonTrivialMove) +{ + bool sourceDestroyed = false; + + struct NonTrivial + { + bool* destroyed; + std::unique_ptr ptr; + + NonTrivial (bool* d, int v) + : destroyed (d) + , ptr (new int (v)) + { + } + + NonTrivial (NonTrivial&& other) noexcept + : destroyed (other.destroyed) + , ptr (std::move (other.ptr)) + { + other.destroyed = nullptr; + } + + ~NonTrivial () + { + if (destroyed) + { + *destroyed = true; + } + } + + int operator() () const + { + return *ptr; + } + }; + + { + Function f2; + + { + Function f1 (NonTrivial (&sourceDestroyed, 42)); + sourceDestroyed = false; + + f2 = std::move (f1); + + EXPECT_FALSE (f1); + EXPECT_FALSE (sourceDestroyed); + } + EXPECT_FALSE (sourceDestroyed); + EXPECT_EQ (f2 (), 42); + } + EXPECT_TRUE (sourceDestroyed); +} + +/** + * @brief Test forwarding. + */ +TEST (Function, forward) +{ + struct Tracker + { + bool lvalue = false; + bool rvalue = false; + + void operator() (int&) + { + lvalue = true; + } + + void operator() (int&&) + { + rvalue = true; + } + }; + + int x = 0; + + { + Tracker t; + Function f (std::ref (t)); + f (x); + EXPECT_TRUE (t.lvalue); + EXPECT_FALSE (t.rvalue); + } + + { + Tracker t; + Function f (std::ref (t)); + f (std::move (x)); + EXPECT_TRUE (t.rvalue); + EXPECT_FALSE (t.lvalue); + } +} + +/** + * @brief Test destroy. + */ +TEST (Function, destroy) +{ + bool destroyed = false; + { + DestructGuard g (&destroyed); + Function f1 (std::move (g)); + destroyed = false; + EXPECT_FALSE (destroyed); + } + EXPECT_TRUE (destroyed); + + destroyed = false; + DestructGuard v (&destroyed); + Function f2 (std::move (v)); + destroyed = false; + f2.reset (); + EXPECT_TRUE (destroyed); + EXPECT_FALSE (f2); + EXPECT_THROW (f2 (), std::bad_function_call); + + destroyed = false; + DestructGuard t (&destroyed); + Function f3 (std::move (t)); + destroyed = false; + f3 = [] () { + }; + EXPECT_TRUE (destroyed); + ASSERT_TRUE (f3); +} + +/** + * @brief Test lambda capture. + */ +TEST (Function, capture) +{ + int x = 10; + int y = 20; + + Function f ([x, &y] () { + return x + y; + }); + + EXPECT_EQ (f (), 30); + y = 40; + EXPECT_EQ (f (), 50); +} + +/** + * @brief Test mutable lambda. + */ +TEST (Function, mutableLambda) +{ + Function f ([value = 0] () mutable { + return ++value; + }); + + EXPECT_EQ (f (), 1); + EXPECT_EQ (f (), 2); + EXPECT_EQ (f (), 3); +} + +/** + * @brief Test swap method. + */ +TEST (Function, swap) +{ + Function f1 ([] { + return 1; + }); + + Function f2 ([] { + return 2; + }); + + f1.swap (f2); + + EXPECT_EQ (f1 (), 2); + EXPECT_EQ (f2 (), 1); + + Function f3; + + f2.swap (f3); + + EXPECT_FALSE (f2); + ASSERT_TRUE (f3); + EXPECT_EQ (f3 (), 1); +} + +/** + * @brief main function. + */ +int main (int argc, char** argv) +{ + testing::InitGoogleTest (&argc, argv); + return RUN_ALL_TESTS (); +}