Skip to content

Commit dec4ae1

Browse files
authored
Merge pull request #520 from bluescarni/pr/type_erasure_improvements
Improvements to the type-erased classes
2 parents 546a549 + 639a1c7 commit dec4ae1

23 files changed

+1289
-1168
lines changed

.github/workflows/gha_ci.yml

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,20 +7,20 @@ on:
77
branches:
88
- master
99
jobs:
10-
osx_13_x86:
11-
runs-on: macos-13
10+
osx_x86:
11+
runs-on: macos-15-intel
1212
steps:
1313
- uses: actions/checkout@v4
1414
- name: Build
1515
run: bash tools/gha_osx_x86.sh
16-
osx_13_static_x86:
17-
runs-on: macos-13
16+
osx_static_x86:
17+
runs-on: macos-15-intel
1818
steps:
1919
- uses: actions/checkout@v4
2020
- name: Build
2121
run: bash tools/gha_osx_x86_static.sh
22-
osx_14_static_arm64:
23-
runs-on: macos-14
22+
osx_static_arm64:
23+
runs-on: macos-latest
2424
steps:
2525
- uses: actions/checkout@v4
2626
- name: Build

CMakeLists.txt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,6 @@ set(HEYOKA_SRC_FILES
188188
"${CMAKE_CURRENT_SOURCE_DIR}/src/detail/div.cpp"
189189
"${CMAKE_CURRENT_SOURCE_DIR}/src/detail/sub.cpp"
190190
"${CMAKE_CURRENT_SOURCE_DIR}/src/detail/vector_math.cpp"
191-
"${CMAKE_CURRENT_SOURCE_DIR}/src/detail/empty_callable_s11n.cpp"
192191
"${CMAKE_CURRENT_SOURCE_DIR}/src/detail/setup_variational_ics.cpp"
193192
"${CMAKE_CURRENT_SOURCE_DIR}/src/detail/tm_data.cpp"
194193
"${CMAKE_CURRENT_SOURCE_DIR}/src/detail/debug.cpp"

doc/tut_s11n.rst

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,9 @@ it in heyoka's serialisation system. This is accomplished through the use of the
156156
:lines: 30-31
157157

158158
The ``HEYOKA_S11N_CALLABLE_EXPORT()`` macro takes as first input argument the name of the class
159-
being registered (``my_callback`` in this case). The remaining arguments are the signature
159+
being registered (``my_callback`` in this case). The second input argument is a boolean flag that,
160+
for event callbacks, must always be ``false`` (this flag indicates that the callback has a *mutable* - i.e.,
161+
non-``const`` - call operator). The remaining arguments are the signature
160162
of the callback: ``void`` is the return type, ``taylor_adaptive<double> &``, ``double``
161163
and ``int`` its argument types. Note that this macro must be invoked in the
162164
root namespace and all arguments should be spelled out as fully-qualified

include/heyoka/callable.hpp

Lines changed: 122 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -43,107 +43,124 @@ inline constexpr bool is_any_std_func<std::function<R(Args...)>> = true;
4343
template <typename>
4444
inline constexpr bool is_any_callable = false;
4545

46-
// An empty struct used in the default initialisation
47-
// of callable objects.
48-
// NOTE: we use this rather than, e.g., a null function
49-
// pointer so that we can enable serialisation of
50-
// default-constructed callables.
51-
struct HEYOKA_DLL_PUBLIC_INLINE_CLASS empty_callable {
52-
template <typename Archive>
53-
void serialize(Archive &, unsigned)
54-
{
55-
}
56-
};
46+
// Base interface for callable objects.
47+
//
48+
// The base interface contains the bool conversion operator.
49+
//
50+
// NOLINTNEXTLINE(cppcoreguidelines-special-member-functions,hicpp-special-member-functions,cppcoreguidelines-virtual-class-destructor)
51+
struct HEYOKA_DLL_PUBLIC_INLINE_CLASS base_callable_iface {
52+
virtual explicit operator bool() const noexcept = 0;
5753

58-
// Default (empty) implementation of the callable interface.
59-
template <typename, typename, typename, typename, typename...>
60-
struct HEYOKA_DLL_PUBLIC_INLINE_CLASS callable_iface_impl {
54+
// Default implementation.
55+
template <typename Base, typename T>
56+
struct impl : public Base {
57+
explicit operator bool() const noexcept final
58+
{
59+
// NOTE: make sure to fully unwrap T (including const qualifiers), otherwise we will misdetect
60+
// callable/std::function below if we are wrapping a const reference.
61+
using unrefT = tanuki::unwrap_cvref_t<T>;
62+
63+
if constexpr (std::is_pointer_v<unrefT> || std::is_member_pointer_v<unrefT>) {
64+
return getval(this) != nullptr;
65+
} else if constexpr (is_any_callable<unrefT> || is_any_std_func<unrefT>) {
66+
return static_cast<bool>(getval(this));
67+
} else {
68+
return true;
69+
}
70+
}
71+
};
6172
};
6273

63-
// Implementation of the callable interface for invocable objects.
64-
template <typename Base, typename Holder, typename T, typename R, typename... Args>
65-
requires std::is_invocable_r_v<R, std::remove_reference_t<std::unwrap_reference_t<T>> &, Args...>
66-
// NOTE: also require copy constructibility like
67-
// std::function does.
68-
&& std::copy_constructible<T>
69-
struct HEYOKA_DLL_PUBLIC_INLINE_CLASS callable_iface_impl<Base, Holder, T, R, Args...> : public Base {
70-
explicit operator bool() const noexcept final
71-
{
72-
using unrefT = std::remove_reference_t<std::unwrap_reference_t<T>>;
74+
// The two interfaces for const and mutable callable objects.
75+
template <typename R, typename... Args>
76+
struct HEYOKA_DLL_PUBLIC_INLINE_CLASS const_callable_iface : base_callable_iface {
77+
virtual R operator()(Args... args) const = 0;
78+
};
7379

74-
if constexpr (std::is_pointer_v<unrefT> || std::is_member_pointer_v<unrefT>) {
75-
return getval<Holder>(this) != nullptr;
76-
} else if constexpr (is_any_callable<unrefT> || is_any_std_func<unrefT>) {
77-
return static_cast<bool>(getval<Holder>(this));
78-
} else {
79-
return true;
80-
}
81-
}
82-
R operator()(Args... args) final
83-
{
84-
using unrefT = std::remove_reference_t<std::unwrap_reference_t<T>>;
85-
86-
// Check if this is empty before invoking the call operator.
87-
// NOTE: no check needed here for std::function or callable: in case
88-
// of an empty object, the std::bad_function_call exception will be
89-
// thrown by the call operator of the object.
90-
if constexpr (std::is_pointer_v<unrefT> || std::is_member_pointer_v<unrefT>) {
91-
if (getval<Holder>(this) == nullptr) {
92-
throw std::bad_function_call{};
93-
}
94-
}
80+
template <typename R, typename... Args>
81+
struct HEYOKA_DLL_PUBLIC_INLINE_CLASS mutable_callable_iface : base_callable_iface {
82+
virtual R operator()(Args... args) = 0;
83+
};
9584

96-
if constexpr (std::is_same_v<R, void>) {
97-
static_cast<void>(std::invoke(getval<Holder>(this), std::forward<Args>(args)...));
98-
} else {
99-
return std::invoke(getval<Holder>(this), std::forward<Args>(args)...);
85+
// Implementation of the call operator for the callable interface. We need both a const and a mutable variant with
86+
// identical code, so we move the implementation outside.
87+
template <typename T, typename R, typename Impl, typename... Args>
88+
R callable_call_operator(Impl *self, Args &&...args)
89+
{
90+
using unrefT = tanuki::unwrap_cvref_t<T>;
91+
92+
// Check if this is empty before invoking the call operator.
93+
//
94+
// NOTE: no check needed here for std::function or callable: in case of an empty object, the
95+
// std::bad_function_call exception will be thrown by the call operator of the object.
96+
if constexpr (std::is_pointer_v<unrefT> || std::is_member_pointer_v<unrefT>) {
97+
if (getval(self) == nullptr) [[unlikely]] {
98+
throw std::bad_function_call{};
10099
}
101100
}
102-
};
103101

104-
// Implementation of the callable interface for the empty callable.
105-
template <typename Base, typename Holder, typename R, typename... Args>
106-
struct HEYOKA_DLL_PUBLIC_INLINE_CLASS callable_iface_impl<Base, Holder, empty_callable, R, Args...> : public Base {
107-
// NOTE: the empty callable is always empty and always results
108-
// in an exception being thrown if called.
109-
explicit operator bool() const noexcept final
110-
{
111-
return false;
102+
if constexpr (std::is_same_v<R, void>) {
103+
static_cast<void>(std::invoke(getval(self), std::forward<Args>(args)...));
104+
} else {
105+
return std::invoke(getval(self), std::forward<Args>(args)...);
112106
}
113-
[[noreturn]] R operator()(Args...) final
114-
{
115-
throw std::bad_function_call{};
116-
}
117-
};
107+
}
118108

119109
// Definition of the callable interface.
120-
template <typename R, typename... Args>
110+
//
111+
// This inherits from either const_callable_iface or mutable_callable_iface, depending on the Const flag.
112+
template <bool Const, typename R, typename... Args>
121113
// NOLINTNEXTLINE(cppcoreguidelines-special-member-functions,hicpp-special-member-functions)
122-
struct HEYOKA_DLL_PUBLIC_INLINE_CLASS callable_iface {
123-
virtual R operator()(Args... args) = 0;
124-
virtual explicit operator bool() const noexcept = 0;
125-
126-
template <typename Base, typename Holder, typename T>
127-
using impl = callable_iface_impl<Base, Holder, T, R, Args...>;
114+
struct HEYOKA_DLL_PUBLIC_INLINE_CLASS callable_iface
115+
: std::conditional_t<Const, const_callable_iface<R, Args...>, mutable_callable_iface<R, Args...>> {
116+
// Default (empty) implementation.
117+
template <typename, typename>
118+
struct impl {
119+
};
120+
121+
// Implementation for mutable invocable objects.
122+
template <typename Base, typename T>
123+
requires(!Const)
124+
&& std::is_invocable_r_v<R, tanuki::unwrap_cvref_t<T> &, Args...>
125+
// NOTE: here we are also checking that T is not a const reference wrapper, which would lead to a
126+
// runtime exception when invoking getval() in the call operator. Like this, we move the error to
127+
// compile time.
128+
&& (!is_reference_wrapper<T> || !std::is_const_v<std::remove_reference_t<std::unwrap_reference_t<T>>>)
129+
// NOTE: also require copy constructibility like std::function does.
130+
&& std::copy_constructible<T>
131+
struct impl<Base, T> : base_callable_iface::impl<Base, T> {
132+
R operator()(Args... args) final
133+
{
134+
return callable_call_operator<T, R>(this, std::forward<Args>(args)...);
135+
}
136+
};
137+
138+
// Implementation for const invocable objects.
139+
template <typename Base, typename T>
140+
requires Const
141+
&& std::is_invocable_r_v<R, const tanuki::unwrap_cvref_t<T> &, Args...> && std::copy_constructible<T>
142+
struct impl<Base, T> : base_callable_iface::impl<Base, T> {
143+
R operator()(Args... args) const final
144+
{
145+
return callable_call_operator<T, R>(this, std::forward<Args>(args)...);
146+
}
147+
};
128148
};
129149

130150
// Implementation of the reference interface.
131151
template <typename R, typename... Args>
132152
struct HEYOKA_DLL_PUBLIC_INLINE_CLASS callable_ref_iface {
133-
template <typename Wrap, typename... FArgs>
134-
requires requires(Wrap &&self, FArgs &&...fargs) {
135-
{
136-
std::forward_like<Wrap> (*iface_ptr(std::forward<Wrap>(self)))(std::forward<FArgs>(fargs)...)
137-
} -> std::same_as<R>;
138-
}
139-
R operator()(this Wrap &&self, FArgs &&...fargs)
153+
// NOTE: thanks to the "deducing this" feature, we need just one implementation of the call operator, which works
154+
// for both the const and mutable variants.
155+
template <typename Wrap>
156+
R operator()(this Wrap &&self, Args... args)
140157
{
141158
// NOTE: a wrap in invalid state is considered empty.
142159
if (is_invalid(self)) [[unlikely]] {
143160
throw std::bad_function_call{};
144161
}
145162

146-
return std::forward_like<Wrap>(*iface_ptr(std::forward<Wrap>(self)))(std::forward<FArgs>(fargs)...);
163+
return iface_ptr(std::forward<Wrap>(self))->operator()(std::forward<Args>(args)...);
147164
}
148165

149166
template <typename Wrap>
@@ -159,40 +176,45 @@ struct HEYOKA_DLL_PUBLIC_INLINE_CLASS callable_ref_iface {
159176
};
160177

161178
// Configuration of the callable wrap.
162-
template <typename R, typename... Args>
163-
inline constexpr auto callable_wrap_config = tanuki::config<empty_callable, callable_ref_iface<R, Args...>>{
164-
// Similarly to std::function, ensure that callable can store
165-
// in static storage pointers and reference wrappers.
166-
// NOTE: reference wrappers are not guaranteed to have the size
167-
// of a pointer, but in practice that should always be the case.
168-
// In case this is a concern, static asserts can be added
169-
// in the callable interface implementation.
170-
.static_size = tanuki::holder_size<R (*)(Args...), callable_iface<R, Args...>>,
179+
template <bool Const, typename R, typename... Args>
180+
inline constexpr auto callable_wrap_config = tanuki::config<void, callable_ref_iface<R, Args...>>{
181+
// Similarly to std::function, ensure that callable can store in static storage pointers and reference wrappers.
182+
//
183+
// NOTE: reference wrappers are not guaranteed to have the size of a pointer, but in practice that should always be
184+
// the case. In case this is a concern, static asserts can be added in the callable interface implementation.
185+
.static_size = tanuki::holder_size<R (*)(Args...), callable_iface<Const, R, Args...>>,
186+
.invalid_default_ctor = true,
171187
.pointer_interface = false,
172188
.explicit_ctor = tanuki::wrap_ctor::always_implicit};
173189

174190
// Definition of the callable wrap.
175-
template <typename R, typename... Args>
176-
using callable_wrap_t = tanuki::wrap<callable_iface<R, Args...>, callable_wrap_config<R, Args...>>;
191+
template <bool Const, typename R, typename... Args>
192+
using callable_wrap_t = tanuki::wrap<callable_iface<Const, R, Args...>, callable_wrap_config<Const, R, Args...>>;
177193

178194
// Specialise is_any_callable to detect callables.
179-
template <typename R, typename... Args>
180-
inline constexpr bool is_any_callable<detail::callable_wrap_t<R, Args...>> = true;
195+
template <bool Const, typename R, typename... Args>
196+
inline constexpr bool is_any_callable<detail::callable_wrap_t<Const, R, Args...>> = true;
181197

198+
// Helper to select the const or mutable callable wrap variant.
182199
template <typename T>
183-
struct callable_impl {
200+
struct callable_impl_selector {
184201
};
185202

186203
template <typename R, typename... Args>
187-
struct callable_impl<R(Args...)> {
188-
using type = callable_wrap_t<R, Args...>;
204+
struct callable_impl_selector<R(Args...)> {
205+
using type = callable_wrap_t<false, R, Args...>;
206+
};
207+
208+
template <typename R, typename... Args>
209+
struct callable_impl_selector<R(Args...) const> {
210+
using type = callable_wrap_t<true, R, Args...>;
189211
};
190212

191213
} // namespace detail
192214

193215
template <typename T>
194-
requires(requires() { typename detail::callable_impl<T>::type; })
195-
using callable = typename detail::callable_impl<T>::type;
216+
requires(requires() { typename detail::callable_impl_selector<T>::type; })
217+
using callable = typename detail::callable_impl_selector<T>::type;
196218

197219
HEYOKA_END_NAMESPACE
198220

@@ -203,9 +225,10 @@ HEYOKA_END_NAMESPACE
203225
#endif
204226

205227
// Serialisation macros.
206-
// NOTE: by default, we build a custom name and pass it to TANUKI_S11N_WRAP_EXPORT_KEY2.
207-
// This allows us to reduce the size of the final guid wrt to what TANUKI_S11N_WRAP_EXPORT_KEY
208-
// would synthesise, and thus to ameliorate the "class name too long" issue.
228+
//
229+
// NOTE: by default, we build a custom name and pass it to TANUKI_S11N_WRAP_EXPORT_KEY2. This allows us to reduce the
230+
// size of the final guid wrt to what TANUKI_S11N_WRAP_EXPORT_KEY would synthesise, and thus to ameliorate the "class
231+
// name too long" issue.
209232
#define HEYOKA_S11N_CALLABLE_EXPORT_KEY(udc, ...) \
210233
TANUKI_S11N_WRAP_EXPORT_KEY2(udc, "heyoka::callable<" #__VA_ARGS__ ">@" #udc, \
211234
heyoka::detail::callable_iface<__VA_ARGS__>)

0 commit comments

Comments
 (0)