Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Alternative for std::bind in modern C++

So I'm trying to create a class which has a container for functors of a different type.

This is a simplified version of it.

template<class T>
class Container
{

public:
    template<typename F, typename ... ARGS>
    void addTask(F && func, ARGS && ... args);
private:
    std::deque<std::function<T()>> container;

    //.....
};

template<class T>
template<typename F, typename ... ARGS>
T Container<T>::addTask(F && func, ARGS && ... args);
{
    container.emplace_back(std::bind(f,args...));

    //.....
}

There are still few problems that I cannot solve yet.

  1. Is there a way to remove std::bind and store a different object or a pointer?
  2. Could this be more generic? Can I somehow store functions, which return different objects, in a single container(int,void...)?
  3. Can some of the logic for creating the tasks be executed in compile time?Something like consexpr bind.
like image 839
Petar Velev Avatar asked Sep 08 '17 14:09

Petar Velev


People also ask

Why do we need std :: bind?

std::bind is a Standard Function Objects that acts as a Functional Adaptor i.e. it takes a function as input and returns a new function Object as an output with with one or more of the arguments of passed function bound or rearranged.

What is the use of bind in C++?

Bind function with the help of placeholders helps to manipulate the position and number of values to be used by the function and modifies the function according to the desired output.

What is the return type of std :: bind?

std::bind return type The return type of std::bind holds a member object of type std::decay<F>::type constructed from std::forward<F>(f), and one object per each of args... , of type std::decay<Arg_i>::type, similarly constructed from std::forward<Arg_i>(arg_i).


1 Answers

From a comment by the OP.

There are. This is simplified. I'm using futures and a special container in the real code. It is meant to be used in a multithreading environment

This is called burying the lede.

If you are storing callables to be invoked in other threads, in the other thread you want signature void(). In this thread you want a std::future to be populated.

As for binding arguments, while a number of std functions do this for you, I find it is best to ask for callables with pre-bound arguments. They can do it outside, using std::bind or lambdas or whatever other means they choose.

So this then comes

template<class Func,
  class R = std::decay_t<std::result_of_t<Func const&()>>
>
std::future< R >
addTask( Func&& func ) {
  auto task = std::packaged_task<R()>(std::forward<Func>(func));
  auto ret = task.get_future();
  container.push_back( std::packaged_task<void()>( std::move(task) ) );
  return ret;
}

std::deque< std::packaged_task<void()> > container;

throw in some mutexes and shake and bake.

Here I use std::packaged_task<void()> as a pre-written move-only type-erased container for anything with that signature. We don't use the future it can produce, which is a waste, but it is shorter than writing your own move-only invoke-once owning function object.

I personally just wrote myself a light weight move-only std::function<void()> esque class instead of using std::packaged_task<void()>, but it was probably unwise.

The future returned from addTask gets fullfilled when the packaged_task<R()> is invoked, which is invoked when the packaged_task<void()> is invoked (possibly in another thread).


Outside of the structure, callers can give you any zero-argument callable object.

99 times out of 100, a simple [some_arg]{ some_code; } or even []{ some_code; } works. In complex cases they can mess around with std::bind or C++14 improvements with more complex lambdas.

Putting the storing of the arguments into addTask mixes the responsibility of the thread-task-queue with messing with arguments.

In fact, I'd write a thread-safe queue separately from my thread-pool, and have the thread-pool use it:

template<class T>
struct thread_safe_queue;

struct thread_pool {
  thread_safe_queue< std::packaged_task<void()> > queue;
  // etc
};

In C++17, a replacement for your bind looks like:

[
  func = std::forward<Func>(func),
  args = std::make_tuple( std::forward<Args>(args)... )
]() mutable {
  std::apply( func, std::move(args) );
}

In C++14 you can write notstd::apply pretty easy. Move-into-lambda requires C++14, so if you need to efficiently move arguments you need std bind or a manual function object in C++11.

I will argue that placing the argument binding strongly in the domain of the code using the thread pool is best.

That also permits the thread pool to do things like pass the tasks optional extra arguments, like "cancellation tokens" or the like.

like image 76
Yakk - Adam Nevraumont Avatar answered Sep 29 '22 15:09

Yakk - Adam Nevraumont