From bda78de369cf65c78afa1b1a3cb38cf7c6ab39dc Mon Sep 17 00:00:00 2001 From: BartolomeyKant Date: Fri, 4 Sep 2026 16:11:41 +0500 Subject: [PATCH 1/2] make method ptr callable and fix some issues --- src/aether-miscpp/types/method_ptr.h | 94 ++++++++++++ src/aether-miscpp/types/small_function.h | 180 ++++++++++++----------- 2 files changed, 186 insertions(+), 88 deletions(-) create mode 100644 src/aether-miscpp/types/method_ptr.h diff --git a/src/aether-miscpp/types/method_ptr.h b/src/aether-miscpp/types/method_ptr.h new file mode 100644 index 0000000..97a649f --- /dev/null +++ b/src/aether-miscpp/types/method_ptr.h @@ -0,0 +1,94 @@ +/* + * Copyright 2026 Aethernet Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef AETHER_MISCPP_TYPES_METHOD_PTR_H_ +#define AETHER_MISCPP_TYPES_METHOD_PTR_H_ + +#include +#include + +namespace ae { +/** + * \brief Non-owning wrapper for supported function and member-function + * pointers. + * + * Member-function wrappers retain a raw instance pointer and do not manage its + * lifetime. Only unqualified and const member functions are supported; + * volatile-qualified and ref-qualified member functions are unsupported. + */ + +template +struct MethodPtr; + +namespace method_ptr_internal { +template +struct IsMethodPtr : std::false_type {}; + +template +struct IsMethodPtr> : std::true_type {}; +} // namespace method_ptr_internal + +template +concept MethodPtrType = method_ptr_internal::IsMethodPtr::value; + +// Free-function pointer. +template +struct MethodPtr { + static constexpr auto kMethod = Method; + static TRet Invoke(TArgs... args) { + return Method(std::forward(args)...); + } + + TRet operator()(TArgs... args) const { + return Invoke(std::forward(args)...); + } +}; + +// Unqualified class-member function pointer. +template +struct MethodPtr { + static constexpr auto kMethod = Method; + static TRet Invoke(T* instance, TArgs... args) { + return (instance->*Method)(std::forward(args)...); + } + + TRet operator()(TArgs... args) const { + return Invoke(instance, std::forward(args)...); + } + + T* instance; +}; + +// Const class-member function pointer. +template +struct MethodPtr { + static constexpr auto kMethod = Method; + static TRet Invoke(T const* instance, TArgs... args) { + return (instance->*Method)(std::forward(args)...); + } + + TRet operator()(TArgs... args) const { + return Invoke(instance, std::forward(args)...); + } + + T const* instance; +}; + +} // namespace ae + +#endif // AETHER_MISCPP_TYPES_METHOD_PTR_H_ diff --git a/src/aether-miscpp/types/small_function.h b/src/aether-miscpp/types/small_function.h index 6f14d8d..8edab41 100644 --- a/src/aether-miscpp/types/small_function.h +++ b/src/aether-miscpp/types/small_function.h @@ -17,13 +17,19 @@ #ifndef AETHER_MISCPP_TYPES_SMALL_FUNCTION_H_ #define AETHER_MISCPP_TYPES_SMALL_FUNCTION_H_ -#include -#include +#include #include +#include +#include +#include +#include +#include #include +#include -#include "aether-miscpp/types/aligned_storage.h" #include "aether-miscpp/meta/function_signature.h" +#include "aether-miscpp/types/aligned_storage.h" +#include "aether-miscpp/types/method_ptr.h" // IWYU pragma: exports namespace ae { namespace small_function_internal { @@ -44,25 +50,18 @@ struct Invoker { template static TRet InvokeCallable(void* self, TArgs&&... args) { if constexpr (std::is_void_v) { - std::launder(static_cast(self)) - ->operator()(std::forward(args)...); + std::invoke(*std::launder(static_cast(self)), + std::forward(args)...); } else { - return std::launder(static_cast(self)) - ->operator()(std::forward(args)...); + return std::invoke(*std::launder(static_cast(self)), + std::forward(args)...); } } - // Invoke for member functions - template - static TRet InvokeMemberFunction(void* self, TArgs&&... args) { - auto* instance = reinterpret_cast(*static_cast(self)); - return (instance->*Method)(std::forward(args)...); - } - // Invoke for free functions + template static TRet InvokeFreeFunction(void* self, TArgs&&... args) { - auto method = reinterpret_cast( - *static_cast(self)); + auto method = *std::launder(static_cast(self)); return method(std::forward(args)...); } }; @@ -71,21 +70,14 @@ template static void Manage(void* a, void* b, Operation op) noexcept { switch (op) { case Operation::kMove: { - { - if constexpr (std::is_trivially_move_constructible_v) { - std::memcpy(b, a, sizeof(TCallable)); - } else { - new (b) - TCallable{std::move(*std::launder(static_cast(a)))}; - } - break; - } - case Operation::kDestroy: { - if constexpr (!std::is_trivially_destructible_v) { - std::destroy_at(std::launder(static_cast(a))); - } - break; + new (b) TCallable{std::move(*std::launder(static_cast(a)))}; + break; + } + case Operation::kDestroy: { + if constexpr (!std::is_trivially_destructible_v) { + std::destroy_at(std::launder(static_cast(a))); } + break; } } } @@ -96,37 +88,30 @@ static constexpr auto VTableForT = VTable{ Manage, }; -template -static constexpr auto VTableForClassMember = VTable{ - Invoker::template InvokeMemberFunction, - Manage, -}; - -template +template static constexpr auto VTableForFreeFunction = VTable{ - Invoker::InvokeFreeFunction, - Manage, -}; -} // namespace small_function_internal - -template -struct MethodPtr; - -// class member function pointer -template -struct MethodPtr { - static constexpr auto kMethod = Method; - T* instance; + Invoker::template InvokeFreeFunction, + Manage, }; -// class member function pointer const -template -struct MethodPtr { - static constexpr auto kMethod = Method; - T* instance; -}; +template +/** + * \brief Whether a callable can be stored and invoked by SmallFunction. + * + * For non-void results, the invocation result must be implicitly convertible + * to TRet; explicit-only conversions are rejected. Void results accept and + * discard any invocation result, including results requiring explicit + * conversion. + */ +concept CompatibleCallable = + std::constructible_from && + std::is_nothrow_constructible_v && + std::is_nothrow_move_constructible_v && + std::invocable && + (std::is_void_v || + std::is_convertible_v, TRet>); +} // namespace small_function_internal static constexpr std::size_t kDefaultSize = sizeof(void*) * 4; static constexpr std::size_t kDefaultAlignment = alignof(void*); @@ -151,45 +136,65 @@ class SmallFunction { } } + /** + * \brief Construction for any callable types except MethodPtr. + * By callable it means any type with operator() defined, like lambda or any + * user provided functor. + */ template - requires(requires { - { std::invocable }; - { std::same_as, TRet> }; - { !std::same_as, SmallFunction> }; - }) - SmallFunction(TFunctor&& functor) noexcept + requires(!MethodPtrType> && + !std::same_as, SmallFunction> && + small_function_internal::CompatibleCallable< + std::decay_t, TFunctor, TRet, TArgs...>) + SmallFunction(TFunctor&& functor) noexcept // NOLINT(*explicit*) : vtable_{&small_function_internal::VTableForT, TRet, TArgs...>} { using Type = std::decay_t; - static_assert(sizeof(Type) <= Size, "TFunctor must fit into storage"); - + static_assert(sizeof(Type) <= Size, + "SmallFunction target size exceeds storage size"); + static_assert(alignof(Type) <= Alignment, + "SmallFunction target alignment exceeds storage alignment"); new (storage_.data()) Type{std::forward(functor)}; } - SmallFunction(TRet (*func_ptr)(TArgs...)) noexcept + /** + * \brief Construction for MethodPtr types. + * This distincts from the any callable types constructor just for future + * development and its own static assert message. + */ + template + requires(MethodPtrType> && + small_function_internal::CompatibleCallable, + MPtr, TRet, TArgs...>) + SmallFunction(MPtr&& method_ptr) noexcept // NOLINT(*explicit*) + : vtable_{&small_function_internal::VTableForT, TRet, + TArgs...>} { + using Type = std::decay_t; + static_assert(sizeof(Type) <= Size, + "SmallFunction MethodPtr target size exceeds storage size"); + static_assert( + alignof(Type) <= Alignment, + "SmallFunction MethodPtr target alignment exceeds storage alignment"); + new (storage_.data()) Type{std::forward(method_ptr)}; + } + + /** + * \brief Construction for free function + * It accepts any function pointer type and stores it in the storage. + */ + SmallFunction(TRet (*func_ptr)(TArgs...)) noexcept // NOLINT(*explicit*) : vtable_{ - &small_function_internal::VTableForFreeFunction} { + &small_function_internal::VTableForFreeFunction} { using FuncType = decltype(func_ptr); - static_assert(sizeof(FuncType) <= Size, "pointer must fit into storage"); + static_assert(sizeof(FuncType) <= Size, + "SmallFunction function pointer size exceeds storage size"); + static_assert( + alignof(FuncType) <= Alignment, + "SmallFunction function pointer alignment exceeds storage alignment"); new (storage_.data()) FuncType{func_ptr}; } - template - SmallFunction(MethodPtr func_ptr) noexcept - : vtable_{&small_function_internal::VTableForClassMember} { - static_assert(Size >= sizeof(T*), "pointer must fit into storage"); - new (storage_.data()) T* {func_ptr.instance}; - } - - template - SmallFunction(MethodPtr func_ptr) noexcept - : vtable_{&small_function_internal::VTableForClassMember} { - static_assert(Size >= sizeof(T*), "pointer must fit into storage"); - new (storage_.data()) T* {func_ptr.instance}; - } - SmallFunction(SmallFunction const& other) = delete; SmallFunction(SmallFunction&& other) noexcept : vtable_{other.vtable_} { @@ -222,8 +227,7 @@ class SmallFunction { */ TRet operator()(TArgs... args) const { assert(vtable_ != nullptr && "SmallFunction is not initialized"); - return vtable_->invoke(const_cast(storage_.data()), - std::forward(args)...); + return vtable_->invoke(storage_.data(), std::forward(args)...); } explicit operator bool() const noexcept { return vtable_ != nullptr; } @@ -238,7 +242,7 @@ class SmallFunction { } VTable const* vtable_{}; - Storage storage_{}; + mutable Storage storage_{}; }; template From fc16baeb77a3dcf1312bd666dfcf722b2d32e31c Mon Sep 17 00:00:00 2001 From: BartolomeyKant Date: Fri, 4 Sep 2026 16:11:53 +0500 Subject: [PATCH 2/2] fix tests for small function and method ptr --- tests/test-types/CMakeLists.txt | 1 + tests/test-types/main.cpp | 2 + tests/test-types/test-method-ptr.cpp | 71 +++++++++++++ tests/test-types/test-small-function.cpp | 127 +++++++++++++++++++++-- 4 files changed, 192 insertions(+), 9 deletions(-) create mode 100644 tests/test-types/test-method-ptr.cpp diff --git a/tests/test-types/CMakeLists.txt b/tests/test-types/CMakeLists.txt index e323243..5cbb2b1 100644 --- a/tests/test-types/CMakeLists.txt +++ b/tests/test-types/CMakeLists.txt @@ -17,6 +17,7 @@ cmake_minimum_required( VERSION 3.16 ) list(APPEND test_srcs main.cpp test-aligned-storage.cpp + test-method-ptr.cpp test-small-function.cpp test-result.cpp ) diff --git a/tests/test-types/main.cpp b/tests/test-types/main.cpp index 1cf9688..61c0ede 100644 --- a/tests/test-types/main.cpp +++ b/tests/test-types/main.cpp @@ -20,12 +20,14 @@ void setUp() {} void tearDown() {} extern int test_aligned_storage(); +extern int test_method_ptr(); extern int test_small_function(); extern int test_result(); int main() { int res = 0; res += test_aligned_storage(); + res += test_method_ptr(); res += test_small_function(); res += test_result(); return res; diff --git a/tests/test-types/test-method-ptr.cpp b/tests/test-types/test-method-ptr.cpp new file mode 100644 index 0000000..ba9cc08 --- /dev/null +++ b/tests/test-types/test-method-ptr.cpp @@ -0,0 +1,71 @@ +/* + * Copyright 2026 Aethernet Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +#include + +#include "aether-miscpp/types/method_ptr.h" + +namespace ae::test_method_ptr { + +int FreeFunction(int value) { return value + 1; } + +struct Worker { + int Add(int value) { + total += value; + return total; + } + + int AddConst(int value) const { return total + value; } + + int total{}; +}; + +using FreeFunctionWrapper = MethodPtr<&FreeFunction>; +using NonConstMemberWrapper = MethodPtr<&Worker::Add>; +using ConstMemberWrapper = MethodPtr<&Worker::AddConst>; + +// MethodPtr wrappers preserve aggregate trivial representation and recognize +// only their plain types. +static_assert(std::is_aggregate_v); +static_assert(std::is_trivial_v); +static_assert(std::is_aggregate_v); +static_assert(std::is_trivial_v); +static_assert(std::is_aggregate_v); +static_assert(std::is_trivial_v); +static_assert(MethodPtrType); + +// Verifies free, non-const member, and const member wrappers invoke targets. +void test_MethodPtrWrappers() { + auto free_function = FreeFunctionWrapper{}; + TEST_ASSERT_EQUAL_INT(2, free_function(1)); + + auto worker = Worker{}; + auto non_const_member = NonConstMemberWrapper{&worker}; + auto const_member = ConstMemberWrapper{&worker}; + + TEST_ASSERT_EQUAL_INT(3, non_const_member(3)); + TEST_ASSERT_EQUAL_INT(7, const_member(4)); +} + +} // namespace ae::test_method_ptr + +int test_method_ptr() { + UNITY_BEGIN(); + RUN_TEST(ae::test_method_ptr::test_MethodPtrWrappers); + return UNITY_END(); +} diff --git a/tests/test-types/test-small-function.cpp b/tests/test-types/test-small-function.cpp index aed3ea6..b114788 100644 --- a/tests/test-types/test-small-function.cpp +++ b/tests/test-types/test-small-function.cpp @@ -16,6 +16,7 @@ #include +#include #include #include "aether-miscpp/types/small_function.h" @@ -56,6 +57,20 @@ void test_CallALambdas() { TEST_ASSERT_TRUE(f3_called); } +// Verifies a const wrapper can invoke a mutable callable. +void test_ConstWrapperCallsMutableCallable() { + auto invoke_count = 0; + const SmallFunction function{[count = 0, &invoke_count]() mutable { + count++; + invoke_count += count; + }}; + + function(); + function(); + + TEST_ASSERT_EQUAL(3, invoke_count); +} + void test_FunctionsInVector() { using Func = SmallFunction; std::vector funcs; @@ -101,6 +116,31 @@ struct DestructableCallable { bool moved = false; }; +struct ImplicitlyConvertibleResult { + operator int() const { return value; } // NOLINT(*explicit*) + + int value; +}; + +struct ExplicitlyConvertibleResult { + explicit operator int() const { return value; } + + int value; +}; + +struct CallableReturningImplicitResult { + ImplicitlyConvertibleResult operator()(int value) const { return {value}; } +}; + +struct CallableReturningExplicitResult { + static inline int invoke_count; + + ExplicitlyConvertibleResult operator()(int value) const { + invoke_count++; + return {value}; + } +}; + void test_DestructableCallable() { DestructableCallable::destroyed_count = 0; DestructableCallable::move_count = 0; @@ -150,6 +190,7 @@ class Worker { public: static inline int invoke_count; static inline int const_invoke_count; + static inline int result_invoke_count; explicit Worker(int expected) : expected(expected) {} @@ -173,32 +214,61 @@ class Worker { TEST_ASSERT_EQUAL(expected, x); } + // Intentionally non-static to exercise pointers to member functions. + // NOLINTNEXTLINE(readability-convert-member-functions-to-static) + int Return(int x) const { return x; } + + // Intentionally non-static to exercise MethodPtr member pointers. + // NOLINTNEXTLINE(readability-convert-member-functions-to-static) + ImplicitlyConvertibleResult ReturnImplicitResult(int x) const { return {x}; } + + // Intentionally non-static to exercise MethodPtr member pointers. + // NOLINTNEXTLINE(readability-convert-member-functions-to-static) + ExplicitlyConvertibleResult ReturnExplicitResult(int x) const { + result_invoke_count++; + return {x}; + } + int expected; }; void test_ClassMemberFunction() { using Func = SmallFunction; + using Wrapper = MethodPtr<&Worker::Foo>; + constexpr int kTestValue = 12; + static_assert(std::constructible_from); + static_assert(std::constructible_from); Worker::invoke_count = 0; Worker::const_invoke_count = 0; - auto worker = Worker{12}; - auto f1 = Func{MethodPtr<&Worker::Foo>{&worker}}; + auto worker = Worker{kTestValue}; + auto wrapper = Wrapper{&worker}; + auto const const_wrapper = Wrapper{&worker}; + auto f1 = Func{wrapper}; auto f2 = Func{MethodPtr<&Worker::FooConst>{&worker}}; - f1(12); - f2(12); - TEST_ASSERT_EQUAL(1, Worker::invoke_count); + auto f3 = Func{const_wrapper}; + f1(kTestValue); + f2(kTestValue); + f3(kTestValue); + TEST_ASSERT_EQUAL(2, Worker::invoke_count); TEST_ASSERT_EQUAL(1, Worker::const_invoke_count); auto f1_1 = std::move(f1); auto f2_1 = std::move(f2); - f1_1(12); - f2_1(12); - TEST_ASSERT_EQUAL(2, Worker::invoke_count); + f1_1(kTestValue); + f2_1(kTestValue); + TEST_ASSERT_EQUAL(3, Worker::invoke_count); TEST_ASSERT_EQUAL(2, Worker::const_invoke_count); } void test_ClassMemberFunctionRef() { - using Func = SmallFunction; + using Func = SmallFunction; + static_assert(std::constructible_from, + MethodPtr<&Worker::FooRef>>); + static_assert(std::constructible_from, + MethodPtr<&Worker::Return>>); + static_assert(std::constructible_from, + MethodPtr<&Worker::Return>>); Worker::invoke_count = 0; Worker::const_invoke_count = 0; @@ -218,6 +288,43 @@ void test_ClassMemberFunctionRef() { TEST_ASSERT_EQUAL(2, Worker::const_invoke_count); } +// Verifies generic callable and MethodPtr construction share implicit result +// conversion behavior while void wrappers discard invocation results. +void test_ReturnCompatibility() { + using IntFunction = SmallFunction; + using VoidFunction = SmallFunction; + using ImplicitMethod = MethodPtr<&Worker::ReturnImplicitResult>; + using ExplicitMethod = MethodPtr<&Worker::ReturnExplicitResult>; + + static_assert( + std::constructible_from); + static_assert( + !std::constructible_from); + static_assert(std::constructible_from); + static_assert(!std::constructible_from); + static_assert( + std::constructible_from); + static_assert(std::constructible_from); + + auto callable = IntFunction{CallableReturningImplicitResult{}}; + TEST_ASSERT_EQUAL(12, callable(12)); + + auto worker = Worker{0}; + auto method = IntFunction{ImplicitMethod{&worker}}; + TEST_ASSERT_EQUAL(13, method(13)); + + CallableReturningExplicitResult::invoke_count = 0; + Worker::result_invoke_count = 0; + constexpr int kCallableArgument = 14; + constexpr int kMethodArgument = 15; + auto void_callable = VoidFunction{CallableReturningExplicitResult{}}; + auto void_method = VoidFunction{ExplicitMethod{&worker}}; + void_callable(kCallableArgument); + void_method(kMethodArgument); + TEST_ASSERT_EQUAL(1, CallableReturningExplicitResult::invoke_count); + TEST_ASSERT_EQUAL(1, Worker::result_invoke_count); +} + static inline int test_invoke_expected; static inline int test_invoke_count; static void TestInvoke(int x) { @@ -254,11 +361,13 @@ void test_FreeFunction() { int test_small_function() { UNITY_BEGIN(); RUN_TEST(ae::test_small_function::test_CallALambdas); + RUN_TEST(ae::test_small_function::test_ConstWrapperCallsMutableCallable); RUN_TEST(ae::test_small_function::test_FunctionsInVector); RUN_TEST(ae::test_small_function::test_DestructableCallable); RUN_TEST(ae::test_small_function::test_DestructableCallableInVector); RUN_TEST(ae::test_small_function::test_ClassMemberFunction); RUN_TEST(ae::test_small_function::test_ClassMemberFunctionRef); + RUN_TEST(ae::test_small_function::test_ReturnCompatibility); RUN_TEST(ae::test_small_function::test_FreeFunction); return UNITY_END(); }