diff --git a/CMakeLists.txt b/CMakeLists.txt index caea1a625..3760423cf 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -178,6 +178,7 @@ set(HEYOKA_SRC_FILES "${CMAKE_CURRENT_SOURCE_DIR}/src/detail/div.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/src/detail/sub.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/src/detail/vector_math.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/src/detail/empty_callable_s11n.cpp" # NOTE: this will be an empty file in case we are not # building with support for real. "${CMAKE_CURRENT_SOURCE_DIR}/src/detail/real_helpers.cpp" diff --git a/include/heyoka/callable.hpp b/include/heyoka/callable.hpp index 914118b00..2a58d677b 100644 --- a/include/heyoka/callable.hpp +++ b/include/heyoka/callable.hpp @@ -37,6 +37,18 @@ struct is_any_callable; namespace detail { +// An empty struct used in the default initialisation +// of callable objects. +// NOTE: we use this rather than, e.g., a null function +// pointer so that we can enable serialisation of +// default-constructed callables. +struct HEYOKA_DLL_PUBLIC_INLINE_CLASS empty_callable { + template + void serialize(Archive &, unsigned) + { + } +}; + // Declaration of the callable interface template. template struct HEYOKA_DLL_PUBLIC_INLINE_CLASS callable_iface { @@ -60,37 +72,53 @@ template && std::copy_constructible struct HEYOKA_DLL_PUBLIC_INLINE_CLASS callable_iface : virtual callable_iface, tanuki::iface_impl_helper { + explicit operator bool() const noexcept final + { + using unrefT = std::remove_reference_t>; + + if constexpr (std::is_pointer_v || std::is_member_pointer_v) { + return this->value() != nullptr; + } else if constexpr (is_any_callable::value || is_any_std_func_v) { + return static_cast(this->value()); + } else { + return true; + } + } R operator()(Args... args) final { using unrefT = std::remove_reference_t>; + // Check if this is empty before invoking the call operator. + // NOTE: no check needed here for std::function or callable: in case + // of an empty object, the std::bad_function_call exception will be + // thrown by the call operator of the object. if constexpr (std::is_pointer_v || std::is_member_pointer_v) { if (this->value() == nullptr) { throw std::bad_function_call{}; } } - // NOTE: if this->value() is an empty std::function or callable, - // the std::bad_function_call exception will be raised - // by the invocation. - if constexpr (std::is_same_v) { static_cast(std::invoke(this->value(), std::forward(args)...)); } else { return std::invoke(this->value(), std::forward(args)...); } } +}; + +// Implementation of the callable interface for the empty callable. +template +struct HEYOKA_DLL_PUBLIC_INLINE_CLASS callable_iface + : virtual callable_iface { + // NOTE: the empty callable is always empty and always results + // in an exception being thrown if called. explicit operator bool() const noexcept final { - using unrefT = std::remove_reference_t>; - - if constexpr (std::is_pointer_v || std::is_member_pointer_v) { - return this->value() != nullptr; - } else if constexpr (is_any_callable::value || is_any_std_func_v) { - return static_cast(this->value()); - } else { - return true; - } + return false; + } + [[noreturn]] R operator()(Args...) final + { + throw std::bad_function_call{}; } }; @@ -103,6 +131,7 @@ struct HEYOKA_DLL_PUBLIC_INLINE_CLASS callable_ref_iface_impl { auto operator()(FArgs &&...fargs) -> decltype(iface_ptr(*static_cast(this))->operator()(std::forward(fargs)...)) { + // NOTE: a wrap in invalid state is considered empty. if (is_invalid(*static_cast(this))) { throw std::bad_function_call{}; } @@ -112,6 +141,7 @@ struct HEYOKA_DLL_PUBLIC_INLINE_CLASS callable_ref_iface_impl { explicit operator bool() const noexcept { + // NOTE: a wrap in invalid state is considered empty. if (is_invalid(*static_cast(this))) { return false; } else { @@ -144,20 +174,23 @@ struct HEYOKA_DLL_PUBLIC_INLINE_CLASS callable_ref_iface { using type = callable_ref_iface_impl; }; +// Configuration of the callable wrap. +template +inline constexpr auto callable_wrap_config + = tanuki::config::template type>{ + // Similarly to std::function, ensure that callable can store + // in static storage pointers and reference wrappers. + // NOTE: reference wrappers are not guaranteed to have the size + // of a pointer, but in practice that should always be the case. + // In case this is a concern, static asserts can be added + // in the callable interface implementation. + .static_size = tanuki::holder_size, + .pointer_interface = false, + .explicit_generic_ctor = false}; + // Definition of the callable wrap. template -using callable_wrap_t = tanuki::wrap::template type>{ - // Similarly to std::function, ensure that callable can store - // in static storage pointers and reference wrappers. - // NOTE: reference wrappers are not guaranteed to have the size - // of a pointer, but in practice that should always be the case. - // In case this is a concern, static asserts can be added - // in the callable interface implementation. - .static_size = tanuki::holder_size, - .pointer_interface = false, - .explicit_generic_ctor = false}, - R, Args...>; +using callable_wrap_t = tanuki::wrap, R, Args...>; template struct callable_impl { diff --git a/include/heyoka/step_callback.hpp b/include/heyoka/step_callback.hpp index 1544877b6..c9e1cbbd7 100644 --- a/include/heyoka/step_callback.hpp +++ b/include/heyoka/step_callback.hpp @@ -100,7 +100,7 @@ struct HEYOKA_DLL_PUBLIC_INLINE_CLASS step_cb_ifaceT { // Configuration. template -inline constexpr auto step_cb_wrap_config = tanuki::config::template type>{ +inline constexpr auto step_cb_wrap_config = tanuki::config::template type>{ // Similarly to std::function, ensure that callable can store // in static storage pointers and reference wrappers. // NOTE: reference wrappers are not guaranteed to have the size @@ -264,4 +264,26 @@ HEYOKA_S11N_STEP_CALLBACK_EXPORT_KEY(heyoka::step_callback_set, mppp #endif +// Export the s11n keys for default-constructed step callbacks. +HEYOKA_S11N_STEP_CALLBACK_EXPORT_KEY(heyoka::detail::empty_callable, float) +HEYOKA_S11N_STEP_CALLBACK_EXPORT_KEY(heyoka::detail::empty_callable, double) +HEYOKA_S11N_STEP_CALLBACK_EXPORT_KEY(heyoka::detail::empty_callable, long double) + +HEYOKA_S11N_STEP_CALLBACK_BATCH_EXPORT_KEY(heyoka::detail::empty_callable, float) +HEYOKA_S11N_STEP_CALLBACK_BATCH_EXPORT_KEY(heyoka::detail::empty_callable, double) +HEYOKA_S11N_STEP_CALLBACK_BATCH_EXPORT_KEY(heyoka::detail::empty_callable, long double) + +#if defined(HEYOKA_HAVE_REAL128) + +HEYOKA_S11N_STEP_CALLBACK_EXPORT_KEY(heyoka::detail::empty_callable, mppp::real128) +HEYOKA_S11N_STEP_CALLBACK_BATCH_EXPORT_KEY(heyoka::detail::empty_callable, mppp::real128) + +#endif + +#if defined(HEYOKA_HAVE_REAL) + +HEYOKA_S11N_STEP_CALLBACK_EXPORT_KEY(heyoka::detail::empty_callable, mppp::real) + +#endif + #endif diff --git a/include/heyoka/taylor.hpp b/include/heyoka/taylor.hpp index 1ec3a64f5..0a294fa76 100644 --- a/include/heyoka/taylor.hpp +++ b/include/heyoka/taylor.hpp @@ -2006,4 +2006,39 @@ BOOST_CLASS_VERSION(heyoka::taylor_adaptive_batch, heyoka::detail::t #endif +// Export the s11n keys for default-constructed event callbacks. +#define HEYOKA_S11N_EVENT_CALLBACKS_EXPORT_KEY(T) \ + HEYOKA_S11N_CALLABLE_EXPORT_KEY(heyoka::detail::empty_callable, void, heyoka::taylor_adaptive &, T, int) \ + HEYOKA_S11N_CALLABLE_EXPORT_KEY(heyoka::detail::empty_callable, bool, heyoka::taylor_adaptive &, bool, int) + +#define HEYOKA_S11N_BATCH_EVENT_CALLBACKS_EXPORT_KEY(T) \ + HEYOKA_S11N_CALLABLE_EXPORT_KEY(heyoka::detail::empty_callable, void, heyoka::taylor_adaptive_batch &, T, int, \ + std::uint32_t) \ + HEYOKA_S11N_CALLABLE_EXPORT_KEY(heyoka::detail::empty_callable, bool, heyoka::taylor_adaptive_batch &, bool, \ + int, std::uint32_t) + +HEYOKA_S11N_EVENT_CALLBACKS_EXPORT_KEY(float) +HEYOKA_S11N_EVENT_CALLBACKS_EXPORT_KEY(double) +HEYOKA_S11N_EVENT_CALLBACKS_EXPORT_KEY(long double) + +HEYOKA_S11N_BATCH_EVENT_CALLBACKS_EXPORT_KEY(float) +HEYOKA_S11N_BATCH_EVENT_CALLBACKS_EXPORT_KEY(double) +HEYOKA_S11N_BATCH_EVENT_CALLBACKS_EXPORT_KEY(long double) + +#if defined(HEYOKA_HAVE_REAL128) + +HEYOKA_S11N_EVENT_CALLBACKS_EXPORT_KEY(mppp::real128) +HEYOKA_S11N_BATCH_EVENT_CALLBACKS_EXPORT_KEY(mppp::real128) + +#endif + +#if defined(HEYOKA_HAVE_REAL) + +HEYOKA_S11N_EVENT_CALLBACKS_EXPORT_KEY(mppp::real) + +#endif + +#undef HEYOKA_S11N_EVENT_CALLBACKS_EXPORT_KEY +#undef HEYOKA_S11N_BATCH_EVENT_CALLBACKS_EXPORT_KEY + #endif diff --git a/src/detail/empty_callable_s11n.cpp b/src/detail/empty_callable_s11n.cpp new file mode 100644 index 000000000..40590b87a --- /dev/null +++ b/src/detail/empty_callable_s11n.cpp @@ -0,0 +1,90 @@ +// Copyright 2020, 2021, 2022, 2023 Francesco Biscani (bluescarni@gmail.com), Dario Izzo (dario.izzo@gmail.com) +// +// This file is part of the heyoka library. +// +// This Source Code Form is subject to the terms of the Mozilla +// Public License v. 2.0. If a copy of the MPL was not distributed +// with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#include + +#include +#include +#include +#include + +#if defined(HEYOKA_HAVE_REAL128) + +#include + +#endif + +#if defined(HEYOKA_HAVE_REAL) + +#include + +#endif + +// NOTE: this file contains the code for registering default-constructed +// event and step callbacks in the serialisation system. + +// NOLINTBEGIN(cert-err58-cpp) + +// Scalar and batch event callbacks. +#define HEYOKA_S11N_IMPLEMENT_EVENT_CALLBACKS(T) \ + HEYOKA_S11N_CALLABLE_EXPORT_IMPLEMENT(heyoka::detail::empty_callable, void, heyoka::taylor_adaptive &, T, int) \ + HEYOKA_S11N_CALLABLE_EXPORT_IMPLEMENT(heyoka::detail::empty_callable, bool, heyoka::taylor_adaptive &, bool, int) + +#define HEYOKA_S11N_IMPLEMENT_BATCH_EVENT_CALLBACKS(T) \ + HEYOKA_S11N_CALLABLE_EXPORT_IMPLEMENT(heyoka::detail::empty_callable, void, heyoka::taylor_adaptive_batch &, T, \ + int, std::uint32_t) \ + HEYOKA_S11N_CALLABLE_EXPORT_IMPLEMENT(heyoka::detail::empty_callable, bool, heyoka::taylor_adaptive_batch &, \ + bool, int, std::uint32_t) + +HEYOKA_S11N_IMPLEMENT_EVENT_CALLBACKS(float) +HEYOKA_S11N_IMPLEMENT_EVENT_CALLBACKS(double) +HEYOKA_S11N_IMPLEMENT_EVENT_CALLBACKS(long double) + +HEYOKA_S11N_IMPLEMENT_BATCH_EVENT_CALLBACKS(float) +HEYOKA_S11N_IMPLEMENT_BATCH_EVENT_CALLBACKS(double) +HEYOKA_S11N_IMPLEMENT_BATCH_EVENT_CALLBACKS(long double) + +#if defined(HEYOKA_HAVE_REAL128) + +HEYOKA_S11N_IMPLEMENT_EVENT_CALLBACKS(mppp::real128) +HEYOKA_S11N_IMPLEMENT_BATCH_EVENT_CALLBACKS(mppp::real128) + +#endif + +#if defined(HEYOKA_HAVE_REAL) + +HEYOKA_S11N_IMPLEMENT_EVENT_CALLBACKS(mppp::real) + +#endif + +#undef HEYOKA_S11N_IMPLEMENT_EVENT_CALLBACKS +#undef HEYOKA_S11N_IMPLEMENT_BATCH_EVENT_CALLBACKS + +// Scalar and batch step callbacks. +HEYOKA_S11N_STEP_CALLBACK_EXPORT_IMPLEMENT(heyoka::detail::empty_callable, float) +HEYOKA_S11N_STEP_CALLBACK_EXPORT_IMPLEMENT(heyoka::detail::empty_callable, double) +HEYOKA_S11N_STEP_CALLBACK_EXPORT_IMPLEMENT(heyoka::detail::empty_callable, long double) + +HEYOKA_S11N_STEP_CALLBACK_BATCH_EXPORT_IMPLEMENT(heyoka::detail::empty_callable, float) +HEYOKA_S11N_STEP_CALLBACK_BATCH_EXPORT_IMPLEMENT(heyoka::detail::empty_callable, double) +HEYOKA_S11N_STEP_CALLBACK_BATCH_EXPORT_IMPLEMENT(heyoka::detail::empty_callable, long double) + +#if defined(HEYOKA_HAVE_REAL128) + +HEYOKA_S11N_STEP_CALLBACK_EXPORT_IMPLEMENT(heyoka::detail::empty_callable, mppp::real128) +HEYOKA_S11N_STEP_CALLBACK_BATCH_EXPORT_IMPLEMENT(heyoka::detail::empty_callable, mppp::real128) + +#endif + +#if defined(HEYOKA_HAVE_REAL) + +HEYOKA_S11N_STEP_CALLBACK_EXPORT_IMPLEMENT(heyoka::detail::empty_callable, mppp::real) + +#endif + +// NOLINTEND(cert-err58-cpp) diff --git a/test/callable.cpp b/test/callable.cpp index 3cc9d684d..310e76ad0 100644 --- a/test/callable.cpp +++ b/test/callable.cpp @@ -182,7 +182,7 @@ TEST_CASE("callable type idx") { callable c; - REQUIRE(c.get_type_index() == typeid(void (*)())); + REQUIRE(c.get_type_index() == typeid(detail::empty_callable)); auto f = []() {}; @@ -207,6 +207,7 @@ struct foo_s11n { }; HEYOKA_S11N_CALLABLE_EXPORT(foo_s11n, int, int) +HEYOKA_S11N_CALLABLE_EXPORT(heyoka::detail::empty_callable, int, int) TEST_CASE("callable s11n") { @@ -235,6 +236,30 @@ TEST_CASE("callable s11n") REQUIRE(!!c); REQUIRE(c(1) == 101); } + + // Test an empty callable too. + { + callable c; + + std::stringstream ss; + + { + boost::archive::binary_oarchive oa(ss); + + oa << c; + } + + c = callable(foo_s11n{100}); + REQUIRE(c); + + { + boost::archive::binary_iarchive ia(ss); + + ia >> c; + } + + REQUIRE(!c); + } } struct vfunc { @@ -245,8 +270,9 @@ struct vfunc { TEST_CASE("callable extract") { callable c; - REQUIRE(c.extract() != nullptr); - REQUIRE(std::as_const(c).extract() != nullptr); + REQUIRE(c.extract() == nullptr); + REQUIRE(c.extract() != nullptr); + REQUIRE(std::as_const(c).extract() == nullptr); c = callable(blap); REQUIRE(c.extract() != nullptr); diff --git a/test/step_callback.cpp b/test/step_callback.cpp index 3d9ba6af8..fc276ed05 100644 --- a/test/step_callback.cpp +++ b/test/step_callback.cpp @@ -87,7 +87,7 @@ TEST_CASE("step_callback basics") REQUIRE(!std::is_constructible_v, void>); REQUIRE(!std::is_constructible_v, int, int>); - REQUIRE(step_cb.get_type_index() == typeid(bool (*)(taylor_adaptive &))); + REQUIRE(step_cb.get_type_index() == typeid(detail::empty_callable)); // Copy construction of empty callback. auto step_cb2 = step_cb; @@ -286,6 +286,30 @@ TEST_CASE("step_callback s11n") REQUIRE(step_cb.template extract() != nullptr); } + // Empty step callback test. + { + step_callback step_cb; + + std::stringstream ss; + + { + boost::archive::binary_oarchive oa(ss); + + oa << step_cb; + } + + step_cb = step_callback{cb2{}}; + REQUIRE(step_cb); + + { + boost::archive::binary_iarchive ia(ss); + + ia >> step_cb; + } + + REQUIRE(!step_cb); + } + { step_callback_batch step_cb(cb2{}); @@ -309,6 +333,30 @@ TEST_CASE("step_callback s11n") REQUIRE(!!step_cb); REQUIRE(step_cb.template extract() != nullptr); } + + // Empty step callback test. + { + step_callback_batch step_cb; + + std::stringstream ss; + + { + boost::archive::binary_oarchive oa(ss); + + oa << step_cb; + } + + step_cb = step_callback_batch{cb2{}}; + REQUIRE(step_cb); + + { + boost::archive::binary_iarchive ia(ss); + + ia >> step_cb; + } + + REQUIRE(!step_cb); + } }; tuple_for_each(fp_types, tester); diff --git a/test/taylor_t_event.cpp b/test/taylor_t_event.cpp index 4b7046365..81429a57a 100644 --- a/test/taylor_t_event.cpp +++ b/test/taylor_t_event.cpp @@ -1113,29 +1113,57 @@ TEST_CASE("t s11n") auto [x, v] = make_vars("x", "v"); - t_event ev(v, kw::callback = s11n_callback{}, kw::direction = event_direction::positive, - kw::cooldown = fp_t(100)); + { + t_event ev(v, kw::callback = s11n_callback{}, kw::direction = event_direction::positive, + kw::cooldown = fp_t(100)); - std::stringstream ss; + std::stringstream ss; - { - boost::archive::binary_oarchive oa(ss); + { + boost::archive::binary_oarchive oa(ss); - oa << ev; - } + oa << ev; + } - ev = t_event(v + x); + ev = t_event(v + x); - { - boost::archive::binary_iarchive ia(ss); + { + boost::archive::binary_iarchive ia(ss); - ia >> ev; + ia >> ev; + } + + REQUIRE(ev.get_expression() == v); + REQUIRE(ev.get_direction() == event_direction::positive); + REQUIRE(ev.get_callback().get_type_index() == typeid(s11n_callback)); + REQUIRE(ev.get_cooldown() == fp_t(100)); } - REQUIRE(ev.get_expression() == v); - REQUIRE(ev.get_direction() == event_direction::positive); - REQUIRE(ev.get_callback().get_type_index() == typeid(s11n_callback)); - REQUIRE(ev.get_cooldown() == fp_t(100)); + // Try also a terminal event with empty callback. + { + t_event ev(v, kw::direction = event_direction::positive, kw::cooldown = fp_t(100)); + + std::stringstream ss; + + { + boost::archive::binary_oarchive oa(ss); + + oa << ev; + } + + ev = t_event(v + x); + + { + boost::archive::binary_iarchive ia(ss); + + ia >> ev; + } + + REQUIRE(ev.get_expression() == v); + REQUIRE(ev.get_direction() == event_direction::positive); + REQUIRE(!ev.get_callback()); + REQUIRE(ev.get_cooldown() == fp_t(100)); + } }; tuple_for_each(fp_types, tester);