Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to initialize delegate with function pointer #2

Open
quangr opened this issue Aug 1, 2022 · 2 comments
Open

How to initialize delegate with function pointer #2

quangr opened this issue Aug 1, 2022 · 2 comments

Comments

@quangr
Copy link

quangr commented Aug 1, 2022

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
Copy link
Owner

bitwizeshift commented Aug 3, 2022

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 Delegate now needs to have storage for a void(*)() conditionally instead of a void* for the instance. It's useful to use a union for this purpose since you will only ever use one of them depending on what is being done.
  • The stub can't take const void* as the first argument anymore, since it may conditionally need that void(*)(). TO account for this, you can just pass a reference to const delegate itself, and then pull the correct argument from the union.

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
Copy link
Author

quangr commented Aug 3, 2022

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!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants