etl icon indicating copy to clipboard operation
etl copied to clipboard

FR: a constructor for delegate with a freestanding function

Open positron96 opened this issue 1 year ago • 10 comments

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.

positron96 avatar Oct 02 '24 09:10 positron96

I'll take a look to see if it is possible.

jwellbelove avatar Oct 02 '24 09:10 jwellbelove

I've been doing C++ for over 20 years, and it still manages to surprise me sometimes, especially convoluted template meta-programming!

jwellbelove avatar Oct 02 '24 09:10 jwellbelove

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));

jwellbelove avatar Oct 04 '24 09:10 jwellbelove

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

https://godbolt.org/z/PTxMcexsW

jwellbelove avatar Oct 04 '24 13:10 jwellbelove

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<>

positron96 avatar Oct 07 '24 10:10 positron96

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>());

jwellbelove avatar Oct 07 '24 14:10 jwellbelove

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);

jwellbelove avatar Oct 09 '24 11:10 jwellbelove

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>();

positron96 avatar Oct 09 '24 12:10 positron96

Yes, that's correct. make_delegate deduces the function signature from the function pointer, so that it doesn't have to be explicitly declared.

jwellbelove avatar Oct 09 '24 12:10 jwellbelove

Ok. Doesn't completely solve it, but this will make using delegates easier!

positron96 avatar Oct 09 '24 14:10 positron96

Actually, I've discovered that the syntax is only valid for C++17 and up.

jwellbelove avatar Oct 22 '24 15:10 jwellbelove

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.

BartolomeyKant avatar Apr 02 '25 06:04 BartolomeyKant

That's an interesting idea. I wonder if it can be achieved without destroying constexpr compatibility?

jwellbelove avatar Apr 02 '25 11:04 jwellbelove

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.

BartolomeyKant avatar Apr 02 '25 11:04 BartolomeyKant

I think I found a better solution with union https://godbolt.org/z/xTMobd7Td

BartolomeyKant avatar Apr 02 '25 12:04 BartolomeyKant

Version 20.41.0

jwellbelove avatar May 22 '25 08:05 jwellbelove