Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Using C++20 concepts to avoid std::function

In the past, when I wanted a callback as a function parameter, I usually decided to use std::function. In rare cases where I definitely never use captures, I used a typedef for a function declaration instead.

So, usually my declaration with a callback parameter looks something like this:

struct Socket
{
  void on_receive(std::function<void(uint8_t*, unsigned long)> cb);
}

However, as far as I know, std::function is actually doing a little work at runtime due to having to resolve the lambda with it's captures to the std::function template and move/copy it's captures (?).

Reading about the new C++ 20 features I figured I might be able to make use of concepts to avoid using std::function and use a constrained parameter for any viable functor.

And this is where my problem comes up: Since I want to work with the callback functor objects sometime in the future, I have to store them. Since I have no definitive type for my callback, my initial thought was to copy (eventuallty move at some point) the functor to heap and use a std::vector<void*> to note where I left them.

template<typename Functor>
concept ReceiveCallback = std::is_invocable_v<Functor, uint8_t*, unsigned long>
                       && std::is_same_v<typename std::invoke_result<Functor, uint8_t*, unsigned long>::type, void>
                       && std::is_copy_constructible_v<Functor>;
struct Socket
{
  std::vector<void*> callbacks;

  template<ReceiveCallback TCallback>
  void on_receive(TCallback const& callback)
  {
    callbacks.push_back(new TCallback(callback));
  }
}

int main(int argc, char** argv)
{
  Socket* sock;
  // [...] inialize socket somehow

  sock->on_receive([](uint8_t* data, unsigned long length)
                   {
                     // NOP for now
                   });

  // [...]
}

While this works well enough, when implementing the method that is supposed to call the functor, I noticed that I have just postponed the issue of the unknown/missing type. As far as my understanding goes, casting a void* to a function pointer or some similar hack should yield UB - How would the compiler know, that I am actually trying to call operator() of a class that is completely unknown?

I thought about storing the (copied) functor along with the function pointer to it's operator() definition, however I have no idea how I could inject the functor as this inside the function, and without it I doubt that captures would work.

Another approach I had was to declare a pure virtual interface that declares the required operator() function. Unfortunately my compiler forbid me to cast my functor to the interface and I don't think there is a legal way to let the lambda derive from it either.

So, is there a way to work this out or am I possibly just misusing the template requirements/concepts feature?

like image 263
Link64 Avatar asked Aug 21 '20 17:08

Link64


People also ask

What is concept in c++ 20?

Writing conceptsConstraint expression can contain constexpr boolean expressions, conjunction/disjunction of other concepts and requires blocks. So for our previous example, we could construct a boolean expression using C++11 type traits or construct a compound concept from C++20 standard library concepts.

Why do we need std :: function?

std::function can hold function objects (including lambdas), as well as function pointers with the correct signature. So it is more versatile.

What are concepts Cpp?

Concepts are a revolutionary approach for writing templates! They allow you to put constraints on template parameters that improve the readability of code, speed up compilation time, and give better error messages. Read on and learn how to use them in your code!


1 Answers

Your initial version used std::function precisely because it erases the type. If you want type erasure (and you clearly do, since you want the user to be able to use any type without your code knowing explicitly what that type is), then you need some form of type erasure. And type erasure isn't free.

Constraints are for templates. You don't want a template function; you want a single function that deals with a type-erased callable.

And for callbacks which have to outlive the call stack of the provider, std::function's overhead is pretty much what you need. That is, the "overhead" is not pointless; it's what permits you to store objects of arbitrary, unknown types in your callback processor.

like image 112
Nicol Bolas Avatar answered Nov 15 '22 06:11

Nicol Bolas