bitwizeshift / Delegate

Delegate is an unbelievably fast, lightweight, and 0-overhead function container.
Boost Software License 1.0
21 stars 1 forks source link

How to initialize delegate with function pointer #2

Open quangr opened 1 year ago

quangr commented 1 year ago

Dear Matthew Rodusek, THANKS for writing such an amazing blog about how to creating delegate type in c++. It really helps a lot. I'm trying to create a timer with a function pointer. I want it to do something like this.

void Task(int a){}
Timer<&task,1000> timer;

However, after mimic the delegate type, the best I can do is something like this.

Timer<decltype(Task)> timer{1000};
timer.bind<&Task>();

I'm wondering if there's something that I can do to bind the function pointer in constructor in C++11. I'm really hoping to get some advice from a c++ expert like you. THANKS again for your fascinating post.

bitwizeshift commented 1 year ago

Hi @quangr, thanks for reaching out -- I'm happy to hear you liked my blog post!

I'm wondering if there's something that I can do to bind the function pointer in constructor in C++11.

Are you wanting just a regular old free-function pointer, or class member-pointers? The restriction of C++11 makes this extremely tricky, although it is possible. The approach taken in this repo for C++17 will largely be similar to what is done in C++11, except when working in C++11 you won't have auto template parameters.


Free Functions

This can be solved in two possible ways: Statically-bound (as template parameters, like the blog-post talks about), or as a runtime-provided value (via the constructor)

Runtime-Provided

Runtime-provided free-functions can be solved with a little "trick", which I actually use in this repo's delegate implementation: All function pointers are legally inter-convertible with other function pointers. E.g. R(*)(Args...) is convertible to UR(*)(UArgs...) and back. The intermediate (incorrect type) form is not callable, but it can be temporarily stored.

With this in mind, what you could do is have a data-member that stores some common pointer type (say, void(*)() for simplicity), and then have a function stub that casts this back to the correct type before calling.

This differs from what I wrote in the blog post in some minor ways:

The stub then just casts the void(*)() back to the original pointer type.

For example:

template <typename R, typename...Args>
class Delegate<R(Args...)> {
public:
    // Would be a good idea to use SFINAE to make sure this is only called with valid function pointers
    template <typename UR, typename...UArgs> 
    Delegate(UR(*)(UArgs...)); // will implement below 

    ...    

private:

    // [expr.reinterpret.cast/6] explicitly allows for a conversion between
    // any two function pointer-types -- provided that the function pointer type
    // is not used through the wrong pointer type.
    // So we normalize all pointers to a simple `void(*)()` to allow for pointers
    // bound in the constructor.
    using any_function = void(*)();

    // Note: the stub now takes 'const Delegate&' now so we can pull out the m_function_pointer
    using stub_function = R(*)(const Delegate&, Args...);

    union {
        // .. other storage types ...
        any_function m_function_pointer;
    };
    stub_function m_stub;

    // These template parameters are used to cast back to the original function pointer
    // type
    template <typename UR, typename...UArgs>
    static auto function_pointer_stub(const delegate& d, Args...args) -> R {

        // cast back to the original type before calling
        const auto original = reinterpret_cast<UR(*)(UArgs...)>(d.m_function_pointer);

        return (*original)(std::forward<Args>(args)...);
    }
};

Then your constructor just becomes:

template <typename R, typename...Args>
template <typename UR, typename...UArgs>
Delegate<R(Args...)>::Delegate(UR(*fn)(UArgs...)) 
    : m_any_function{reinterpret_cast<any_function>(fn)},
      m_stub{&function_pointer_stub<UR,UArgs...>}
{

}

With this, it should allow you to pass any function pointer to a Delegate's constructor.

Working Example

The above supports conversion-based binding (e.g. you can bind an int(*)(int) to a long(*)(long) because the types are similar). If you don't care for this support, you can drop the UR and UArgs... arguments and just make the constructor accept R(*)(Args...) and the stub function cast back to this type every time.

Statically-Specified

Statically-specified values in the constructor are a whole different class of problem. The issue is that the constructor needs the context of the template non-type argument, but doesn't have it.

The way this library solves the problem is by creating bind_target struct types that hold the data, so that an intermediate type can encode the type and be known in the constructor. This changes the syntax a bit to be:

// Note the 'bind<...>()' call
delegate<std::size_t(const char*)> d{bind<&std::strlen>()};

This is easy to do in C++17 with auto template parameters, since you can deduce the type as it's specified. C++11 has no easy/nice way. The closest you can achieve is manually specifying the type before binding it first, something like:

delegate<std::size_t(const char*)> d{bind<std::size_t(const char*), &std::strlen>()};

It works, but it's not quite as nice. The implementation of this would look something like:

template <typename Fn, Fn* FunctionPointer>
struct function_bind_target {};

// User specified R(Args...) as the first argument, then a pointer of that type
template <typename Fn, Fn* FunctionPointer>
constexpr auto bind() -> function_bind_target<Fn,FunctionPointer> { return {}; }

...

template <typename R, typename...Args>
class Delegate<R(Args...)> {
    ...
    template <typename UR, typename...UArgs, UR(*FunctionPointer)(UArgs...)>
    Delegate(function_bind_target<UR(UArgs...),FunctionPointer>)
      : m_stub{nonmember_stub<UR(UArgs...),FunctionPointer>} 
    {

    }
    ...
}

Working Example

Member Functions

Runtime-Provided

Runtime-specified member pointers can't really be done without heap allocation. Unlike function pointers, member pointers do not provide any simple guarantee of conversions -- meaning there's no nice way to erase them at runtime (meaning a constructor of Delegate(R (C::*)(Args...)) is not really doable).

Statically-Specified

This is basically the same answer as the statically-specified point for function pointers above, except you would need to extend it to include the class type.


Anyway, I didn't mean for this post to run on as long as it has -- but hopefully this makes sense. Please let me know if any of that was unclear, or if there's anything else I can help with. Good luck!

quangr commented 1 year ago

WOW, what a detailed AMAZING replay! Thanks for your kindness. It really opened my mind and I appreciate it. I still need to write some code to fully understand it, but there are some tricks I can understand and adapt right away, it is certainly THE most informative reply I have ever gotten from the internet! THANK you very much, have a nice day!