ETLCPP / etl

Embedded Template Library
https://www.etlcpp.com
MIT License
2.25k stars 392 forks source link

FR: a constructor for delegate with a freestanding function #966

Open positron96 opened 1 month ago

positron96 commented 1 month ago

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.

jwellbelove commented 1 month ago

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

jwellbelove commented 1 month ago

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

jwellbelove commented 1 month ago

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 commented 1 month ago

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

positron96 commented 1 month ago

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

jwellbelove commented 1 month ago

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 commented 1 month ago

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);
positron96 commented 1 month ago

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>();
jwellbelove commented 1 month ago

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

positron96 commented 1 month ago

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

jwellbelove commented 1 month ago

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