etl
etl copied to clipboard
FR: a constructor for delegate with a freestanding function
At the moment, you construct a delegate with a freestanding function like this: my_delegate_type::create<my_function>(), and use it like this: add_callback(my_delegate_type::create<my_function>()).
It would be nice to have an implicit constructor for this case, then this ::create distraction could be omitted and simplified to just add_callback(my_function).
As I understand, this syntax is possible for functors and lambdas, but not for freestanding functions. There are probably obstructions to doing this, otherwise you'd probably have implemented it long ago, but my c++-fu is not strong enough to understand it.
I'll take a look to see if it is possible.
I've been doing C++ for over 20 years, and it still manages to surprise me sometimes, especially convoluted template meta-programming!
I've had a go at this, and the fundamental problem is that you cannot explicitly declare the template parameters for a constructor. They must be deduced from the constructor's argument. This works fine for a lambda or functor as there is an instance argument to pass in. This is not possible for a freestanding function.
The only way to simplify construction is to make a lambda or functor wrapper around the free function.
auto my_function_lambda = []() { my_function(); };
add_callback(my_delegate_type(my_function_lambda));
With optimisation, the resulting code is still very efficient, even with the lambda and delegate.
For this code and -O1 optimisation, the lambda and delegate reduce to a direct call of the free function.
#include <iostream>
#include "etl/delegate.h"
void free_void()
{
std::cout << "free_void/n";
}
int main()
{
constexpr auto lambda = []() { free_void(); };
auto d = etl::delegate<void(void)>(lambda);
d();
}
.LC0:
.string "free_void/n"
free_void():
sub rsp, 8
mov edx, 11
mov esi, OFFSET FLAT:.LC0
mov edi, OFFSET FLAT:std::cout
call std::basic_ostream<char, std::char_traits<char> >& std::__ostream_insert<char, std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*, long)
add rsp, 8
ret
main:
sub rsp, 8
call free_void()
mov eax, 0
add rsp, 8
ret
Hi, thanks for looking into this! While you clearly show that adding lambda has no impact on performance, I would argue that it makes source code nosier still, meaning it hides the simple intent of adding a callback by introducing implementation-related entities.
the fundamental problem is that you cannot explicitly declare the template parameters for a constructor. They must be deduced from the constructor's argument.
Do you know if it's possible to explicitly cast a function (pointer) to a type to make compiler deduce template parameters? E.g.:
my_delegate_t callback{my_delegate_t::fn_t(my_function)};
where fn_t is defined as something like
template <typename TReturn, typename... TParams>
class delegate<TReturn(TParams...)> {
using fn_t = TReturn(TParams...);
}
I tried to play with library code, but every time the compiler complained.
However in terms of verbosity this already approaches existing solution with ::create<>
I had a play this morning with creating a make_delgate template function, which seemed to work, although it only works for C++14 and above.
add_callback(etl::make_delegate<my_function>());
I have worked out three etl::make_delegate functions to simplify delegate construction for free and member functions.
Note: These are >= C++14 only.
void free_int(int);
struct S
{
void member(int);
void member_const(int) const;
}
static S s;
auto d1 = etl::make_delegate<free_int>();
auto d2 = etl::make_delegate<S, &S::member, s>();
auto d3 = etl::make_delegate<S, &S::member_const, s>();
auto d4 = etl::make_delegate<S, &S::member>(s);
auto d5 = etl::make_delegate<S, &S::member_const>(s);
So, compared to existing solution, this saves an extra template specialization, right?
auto d = etl::delegate<int(int)>::create<free_func>();
auto d = etl::delegate<int(int)>::create<Test, test, &Test::member_function>();
Yes, that's correct. make_delegate deduces the function signature from the function pointer, so that it doesn't have to be explicitly declared.
Ok. Doesn't completely solve it, but this will make using delegates easier!
Actually, I've discovered that the syntax is only valid for C++17 and up.
I have a Delegate implementation similar to etl's delegate. But I ended up with helper type MethodPtr to deal with such case.
With c++17 it looks like
int foo(float x){};
...
auto delegate = Delegate{MethodPtr<foo>{}};
static_assert(std::is_same_v<Deleate<int(float x)>>, decltype(delegate));
I also thought about another approach. etl::delegate stores two pointers void* object; stub_type stub; If it would be possible somehow convert function pointer to void* we could store it as object and make an invoke function with vice versa conversion.
That's an interesting idea. I wonder if it can be achieved without destroying constexpr compatibility?
Strange. When I have experimented with that a few months ago, I was getting an error about conversion from function pointer to void*. Like on this question https://stackoverflow.com/questions/36645660/why-cant-i-cast-a-function-pointer-to-void
But it works https://godbolt.org/z/rx8vjE7vq Although it's not constexpr.
I think I found a better solution with union https://godbolt.org/z/xTMobd7Td
Version 20.41.0