Bareflank / hypervisor

lightweight hypervisor SDK written in C++ with support for Windows, Linux and UEFI

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

[RFC] Delegate pattern

connojd opened this issue · comments

C++17 added class template argument deduction which enables the compiler to deduce the template arguments of a class. This means that you can call constructors of a template class without explicitly listing the types in angle brackets.

This is how the code looks with the new feature. That version uses inheritance to get a common 'delegate<function_signature>' type usable in containers. There are static_delegate and member_delegate subclasses that derive from the abstract delegate class, and both are marked final to help the compiler devirtualize the calls. If we decide to go down this route, I can add subclasses for lambdas too (although I don't think they are really needed).

Obviously this code is easier to read than the existing method, and IMO we should start using it. If others agree, I can work on a more complete version and then measure the performance impact versus the current implementation.

I 100% agree that these are easier to read, and less verbose that the existing method. 👍 from me on this.

static_delegate does sound a little misleading though (it doesn't have to be a static function, any function will work). Does function_delegate make sense instead?

I'll let @rianquinn comment on any disadvantages this has over the current approach, because he is probably the most familiar with the current implementation.

@connojd HUGE fan of this. Here are my notes:

  • What is the reason for the subclasses? Technically, we should be able to create a single delegate type with different constructors. Then it can just be called delegate and we are done.
  • I agree with @JaredWright that static is misleading. If we need subclasses, I would just call it delegate and member_delegate
  • Based on your implementation and the inclusion of C++17, we should be able to further extend your example such that adding a delegate to a delegate list doesn't actually need you do state the type. For example:
bool foo(vcpu_t *vcpu);
vcpu->add_exit_handler({&foo});

or

class blah{
public:
    bool foo(vcpu_t *vcpu);
};
blah g_blah;
vcpu->add_exit_handler({&blah::foo, &g_blah});

Oh.. one more thing. We should ensure that only valid delegates can be created. Null delegates should not be allowed. Otherwise, we will have to add a bunch of code to check for null which I would like to avoid.

I've created a version using std::function here. The downside of this approach is the malloc for member delegates.

Personally I like the first version better. I can change static to 'free' or something if that would be better.

Lets benchmark this to make sure that it is at least as fast as a vTable call:
https://github.com/PacktPublishing/Hands-On-System-Programming-with-CPP/blob/master/Chapter11/example2.cpp

@rianquinn I've got it working with no std::function thanks to that last link. You can see the usage here and the implementation here

https://github.com/connojd/delegate/blob/master/placement/delegate.h#L65

That is where the type-unsafe type erasure is occurring. This will need to be labeled as a reintepret_cast so that it is explicit that this does not adhere to the Core Guidelines. That said, I don't see how you possibly could implement this otherwise, and I am pretty sure that std::function does basically the same thing. We will simply need to ensure that our testing is really good so that nobody can accidentally overflow the std::array.

In general, I love this. This is exactly what we need. It reduces to a simple vTable call, which is what we currently have. The next step is to merge this in.

One note, can the static functions be inlined?

Let's make sure that the delegate type implements the same functions as std::function for C++17 (as allocators were removed) so that our delegate type basically functions the same as a std::function but with some extra constructors. This way, under the hood, if std::function performs better, the APIs just end up being pass-through. Based on what I have seen in std::function, this should perform better.

BTW... when using a lambda, can you just use the same syntax as std::function prior to C++17. std::delegate<int(int)> d(lambda)? This way the deduction guides are not needed, but you still have a way to support lambdas if you need it.

@rianquinn Yes I don't see a way to extract the Ret, Args... from a lambda type for deduction. That said, it would be very simple to add constructors for lambdas, they just wouldn't be able to use CTAD.

Why do you think it is necessary to support all the functions that std::function does? We only use class-based and normal c functions now

Right, that makes sense. That's fine. Technically, you should be able to do CTAD with std::function as well, so my guess is that is just an issue with the language that will have to be sorted out in future versions. For now, if you can add a lambda by just providing the Ret/Arg type, you should be good to go.

In our case, the Ret/Arg type will be provided anyways as we will be storing delegates in a list, so we will have to be explicit up front so the handler_delegate_t will still be there so a lambda should work without issue.

std::function doesn't support CTAD directly because CTAD only works with the primary template, and the primary template only has one template parameter, but Ret, Args... params are needed for proper deduction. So you're stuck with wrapping std::function in either a function and/or class template

Edit: Apparently I'm misunderstanding the CTAD page, because there are deduction guides defined already for std::function. They also have one for a single type F!! Hmm so it can be done. I'm going to look at how they do that

Ok yeah, when I looked at the implementation, both your delegate and std::function are defined as a class template with the following signature:

template< class R, class... Args >
class function<R(Args...)>;

So they should be the same.

IMO, std::function was implemented using C++11 stuff and as a result, it is just slow and with C++17, there are better ways to implement it. I think our goal should be to ensure that bfn::delegate has the same C++17 interface as std::function so that they are interchangeable (we can add the class member bind operation to the add_handler logic instead since it is just wrapping in a lambda). Once that is done I can do a performance comparison between the two and then pass to the devs for libc++ to see what they say as to why our implementation is faster and possible solutions. If the interface for std::function and bfn::delegate are the same, we should be able to get std::function to work in the future since it would not be an interface issue but just an implementation issue. I think we are close too. Just need to add malloc support for large capture types and the missing helpers.

Another option would be to add a bfn::bind that could be used to add support for class members which would just be a template that wraps the lambda part. Then the add_handler would just have to provide the proper overloads for fun/class member types and then set the delegate as needed. This way, the end user sees the same nice interface for adding delegates but our implementation is closer to the C++ standard, allowing us to eventually ditch our own versions once std::function/std::bind no longer suck

The problem I see with the add_handler is that every add_handler would have to become a template function. That is going to be alot more code to maintain than just defining a delegate type and accepting the type explicitly in add_handler like we do now