the delegate accepts now also functions with a shorter list of parameters

This commit is contained in:
Michele Caini
2019-05-31 00:10:42 +02:00
parent 858bfade6a
commit ce00eee29f
3 changed files with 96 additions and 40 deletions
+29 -16
View File
@@ -38,8 +38,9 @@ A delegate can be used as a general purpose invoker with no memory overhead for
free functions and members provided along with an instance on which to invoke
them.<br/>
It does not claim to be a drop-in replacement for an `std::function`, so do not
expect to use it whenever an `std::function` fits well. However, it can be used
to send opaque delegates around to be used to invoke functions as needed.
expect to use it whenever an `std::function` fits well. That said, it's most
likely even a better fit than an `std::function` in a lot of cases, so expect to
use it quite a lot anyway.
The interface is trivial. It offers a default constructor to create empty
delegates:
@@ -50,7 +51,7 @@ entt::delegate<int(int)> delegate{};
All what is needed to create an instance is to specify the type of the function
the delegate will _contain_, that is the signature of the free function or the
member function one wants to assign to it.
member one wants to assign to it.
Attempting to use an empty delegate by invoking its function call operator
results in undefined behavior or most likely a crash. Before to use a delegate,
@@ -75,7 +76,8 @@ delegate.connect<&my_struct::f>(&instance);
The delegate class accepts also data members, if needed. In this case, the
function type of the delegate is such that the parameter list is empty and the
value of the data member is at least convertible to the return type.<br/>
value of the data member is at least convertible to the return type.
Functions having type equivalent to `void(T *, args...)` are accepted as well.
In this case, `T *` is considered a payload and the function will receive it
back every time it's invoked. In other terms, this works just fine with the
@@ -90,10 +92,25 @@ delegate(42);
```
The function `g` will be invoked with a pointer to `c` and `42`. However, the
function type of the delegate is still `void(int)`, mainly because this is also
the signature of its function call operator.
function type of the delegate is still `void(int)`. This is also the signature
of its function call operator.
To create and initialize a delegate at once, there are also some specialized
Another interesting aspect of the delegate class is that it accepts also
functions with a list of parameters that is shorter than that of the function
type used to specialize the delegate itself.<br/>
This is a nice-to-have feature in a lot of cases. The following code is
therefore perfectly valid:
```cpp
void g() { /* ... */ }
delegate.connect<&g>();
delegate(42);
```
Where the function type of the delegate is `void(int)` as above. It goes without
saying that the extra arguments are silently discarded internally.
To create and initialize a delegate at once, there are a few specialized
constructors. Because of the rules of the language, the listener is provided by
means of the `entt::connect_arg` variable template:
@@ -113,26 +130,22 @@ if(delegate) {
```
Finally, to invoke a delegate, the function call operator is the way to go as
usual:
already shown in the examples above:
```cpp
auto ret = delegate(42);
```
As shown above, listeners do not have to strictly follow the signature of the
In all cases, the listeners don't have to strictly follow the signature of the
delegate. As long as a listener can be invoked with the given arguments to yield
a result that is convertible to the given result type, everything works just
fine.
Probably too much small and pretty poor of functionalities, but the delegate
class can help in a lot of cases and it has shown that it is worth keeping it
within the library.
# Signals
Signal handlers work with naked pointers, function pointers and pointers to
member functions. Listeners can be any kind of objects and users are in charge
of connecting and disconnecting them from a signal to avoid crashes due to
member. Listeners can be any kind of objects and users are in charge of
connecting and disconnecting them from a signal to avoid crashes due to
different lifetimes. On the other side, performance shouldn't be affected that
much by the presence of such a signal handler.<br/>
A signal handler can be used as a private data member without exposing any
@@ -192,7 +205,7 @@ signal.sink().disconnect<&listener::bar>(&instance);
signal.sink().disconnect();
```
As shown above, listeners do not have to strictly follow the signature of the
As shown above, the listeners don't have to strictly follow the signature of the
signal. As long as a listener can be invoked with the given arguments to yield a
result that is convertible to the given result type, everything works just fine.
+52 -24
View File
@@ -2,7 +2,9 @@
#define ENTT_SIGNAL_DELEGATE_HPP
#include <tuple>
#include <cstring>
#include <utility>
#include <algorithm>
#include <functional>
#include <type_traits>
@@ -41,6 +43,20 @@ template<typename Class, typename Type>
auto to_function_pointer(Type Class:: *, const Class *) -> Type(*)();
template<typename>
struct function_extent;
template<typename Ret, typename... Args>
struct function_extent<Ret(*)(Args...)> {
static constexpr auto value = sizeof...(Args);
};
template<typename Func>
constexpr auto function_extent_v = function_extent<Func>::value;
}
@@ -85,7 +101,37 @@ class delegate;
*/
template<typename Ret, typename... Args>
class delegate<Ret(Args...)> {
using proto_fn_type = Ret(const void *, Args...);
using proto_fn_type = Ret(const void *, std::tuple<Args...>);
template<auto Function, std::size_t... Index>
void connect(std::index_sequence<Index...>) ENTT_NOEXCEPT {
data = nullptr;
fn = +[](const void *, std::tuple<Args...> args) -> Ret {
static_assert(std::is_invocable_r_v<Ret, decltype(Function), std::tuple_element_t<Index, std::tuple<Args...>>...>);
// Ret(...) allows void(...) to eat return values and avoid errors
return Ret(std::invoke(Function, std::get<Index>(args)...));
};
}
template<auto Candidate, typename Type, std::size_t... Index>
void connect(Type *value_or_instance, std::index_sequence<Index...>) ENTT_NOEXCEPT {
data = value_or_instance;
fn = +[](const void *payload, std::tuple<Args...> args) -> Ret {
Type *curr = nullptr;
if constexpr(std::is_const_v<Type>) {
curr = static_cast<Type *>(payload);
} else {
curr = static_cast<Type *>(const_cast<void *>(payload));
}
static_assert(std::is_invocable_r_v<Ret, decltype(Candidate), Type *, std::tuple_element_t<Index, std::tuple<Args...>>...>);
// Ret(...) allows void(...) to eat return values and avoid errors
return Ret(std::invoke(Candidate, curr, std::get<Index>(args)...));
};
}
public:
/*! @brief Function type of the delegate. */
@@ -127,13 +173,8 @@ public:
*/
template<auto Function>
void connect() ENTT_NOEXCEPT {
static_assert(std::is_invocable_r_v<Ret, decltype(Function), Args...>);
data = nullptr;
fn = [](const void *, Args... args) -> Ret {
// this allows void(...) to eat return values and avoid errors
return Ret(std::invoke(Function, args...));
};
constexpr auto extent = internal::function_extent_v<decltype(internal::to_function_pointer(std::declval<decltype(Function)>()))>;
connect<Function>(std::make_index_sequence<extent>{});
}
/**
@@ -153,21 +194,8 @@ public:
*/
template<auto Candidate, typename Type>
void connect(Type *value_or_instance) ENTT_NOEXCEPT {
static_assert(std::is_invocable_r_v<Ret, decltype(Candidate), Type *, Args...>);
data = value_or_instance;
fn = [](const void *payload, Args... args) -> Ret {
Type *curr = nullptr;
if constexpr(std::is_const_v<Type>) {
curr = static_cast<Type *>(payload);
} else {
curr = static_cast<Type *>(const_cast<void *>(payload));
}
// this allows void(...) to eat return values and avoid errors
return Ret(std::invoke(Candidate, curr, args...));
};
constexpr auto extent = internal::function_extent_v<decltype(internal::to_function_pointer(std::declval<decltype(Candidate)>(), std::declval<Type *>()))>;
connect<Candidate>(value_or_instance, std::make_index_sequence<extent>{});
}
/**
@@ -204,7 +232,7 @@ public:
*/
Ret operator()(Args... args) const {
ENTT_ASSERT(fn);
return fn(data, args...);
return fn(data, std::forward_as_tuple(args...));
}
/**
+15
View File
@@ -277,3 +277,18 @@ TEST(Delegate, VoidVsNonVoidReturnType) {
ASSERT_TRUE(member);
ASSERT_TRUE(cmember);
}
TEST(Delegate, TheLessTheBetter) {
delegate_functor functor;
entt::delegate<int(int, char)> delegate;
// int delegate_function(const int &);
delegate.connect<&delegate_function>();
ASSERT_EQ(delegate(3, 'c'), 9);
// int delegate_functor::operator()(int);
delegate.connect<&delegate_functor::operator()>(&functor);
ASSERT_EQ(delegate(3, 'c'), 6);
}