How can I store shared packaged_tasks in a container?

I am trying to perform a "task" in the style of std::async and save it in a container. I need to jump through hoops to achieve this, but I think there should be a better way.

 std::vector<std::function<void()>> mTasks; template<class F, class... Args> std::future<typename std::result_of<typename std::decay<F>::type(typename std::decay<Args>::type...)>::type> push(F&& f, Args&&... args) { auto func = std::make_shared<std::packaged_task<typename std::result_of<typename std::decay<F>::type(typename std::decay<Args>::type...)>::type()>>(std::bind(std::forward<F>(f), std::forward<Args>(args)...)); auto future = func->get_future(); // for some reason I get a compilation error in clang if I get rid of the `=, ` in this capture: mTasks.push_back([=, func = std::move(func)]{ (*func)(); }); return future; } 

So, I use bind packaged_task shared_ptr lambda function . How can I do it better / more optimally? Of course, it would be easier if there was a std::function that could perform not a copied, but a movable task. Can I std :: forward args in lambda capture, or do I need to use bind ?

+5
source share
1 answer

There is no such killing as busting.

Step 1: write a friendly SFINAE std::result_of and a function that helps to call through the tuple:

 namespace details { template<size_t...Is, class F, class... Args> auto invoke_tuple( std::index_sequence<Is...>, F&& f, std::tuple<Args>&& args) { return std::forward<F>(f)( std::get<Is>(std::move(args)) ); } // SFINAE friendly result_of: template<class Invocation, class=void> struct invoke_result {}; template<class T, class...Args> struct invoke_result<T(Args...), decltype( void(std::declval<T>()(std::declval<Args>()...)) ) > { using type = decltype( std::declval<T>()(std::declval<Args>()...) ); }; template<class Invocation, class=void> struct can_invoke:std::false_type{}; template<class Invocation> struct can_invoke<Invocation, decltype(void(std::declval< typename invoke_result<Inocation>::type >()))>:std::true_type{}; } template<class F, class... Args> auto invoke_tuple( F&& f, std::tuple<Args>&& args) { return details::invoke_tuple( std::index_sequence_for<Args...>{}, std::forward<F>(f), std::move(args) ); } // SFINAE friendly result_of: template<class Invocation> struct invoke_result:details::invoke_result<Invocation>{}; template<class Invocation> using invoke_result_t = typename invoke_result<Invocation>::type; template<class Invocation> struct can_invoke:details::can_invoke<Invocation>{}; 

Now we have invoke_result_t<A(B,C)> , which is SFINAE friendly result_of_t<A(B,C)> can_invoke<A(B,C)> and can_invoke<A(B,C)> , which just does the check.

Then write move_only_function , the version only for moving std::function :

 namespace details { template<class Sig> struct mof_internal; template<class R, class...Args> struct mof_internal { virtual ~mof_internal() {}; // 4 overloads, because I'm insane: virtual R invoke( Args&&... args ) const& = 0; virtual R invoke( Args&&... args ) & = 0; virtual R invoke( Args&&... args ) const&& = 0; virtual R invoke( Args&&... args ) && = 0; }; template<class F, class Sig> struct mof_pimpl; template<class R, class...Args, class F> struct mof_pimpl<F, R(Args...)>:mof_internal<R(Args...)> { F f; virtual R invoke( Args&&... args ) const& override { return f( std::forward<Args>(args)... ); } virtual R invoke( Args&&... args ) & override { return f( std::forward<Args>(args)... ); } virtual R invoke( Args&&... args ) const&& override { return std::move(f)( std::forward<Args>(args)... ); } virtual R invoke( Args&&... args ) && override { return std::move(f)( std::forward<Args>(args)... ); } }; } template<class R, class...Args> struct move_only_function<R(Args)> { move_only_function(move_only_function const&)=delete; move_only_function(move_only_function &&)=default; move_only_function(std::nullptr_t):move_only_function() {} move_only_function() = default; explicit operator bool() const { return pImpl; } bool operator!() const { return !*this; } R operator()(Args...args) & { return pImpl().invoke(std::forward<Args>(args)...); } R operator()(Args...args)const& { return pImpl().invoke(std::forward<Args>(args)...); } R operator()(Args...args) &&{ return std::move(*this).pImpl().invoke(std::forward<Args>(args)...); } R operator()(Args...args)const&&{ return std::move(*this).pImpl().invoke(std::forward<Args>(args)...); } template<class F,class=std::enable_if_t<can_invoke<decay_t<F>(Args...)>> move_only_function(F&& f): m_pImpl( std::make_unique<details::mof_pimpl<std::decay_t<F>, R(Args...)>>( std::forward<F>(f) ) ) {} private: using internal = details::mof_internal<R(Args...)>; std::unique_ptr<internal> m_pImpl; // rvalue helpers: internal & pImpl() & { return *m_pImpl.get(); } internal const& pImpl() const& { return *m_pImpl.get(); } internal && pImpl() && { return std::move(*m_pImpl.get()); } internal const&& pImpl() const&& { return std::move(*m_pImpl.get()); } // mostly useless }; 

not tested, just spewed code. can_invoke gives the basic SFINAE constructor - you can add "the return type converts correctly" and "void return type means we ignore the return" if you want.

Now we will recycle your code. First, your task is functions only for movement, not functions:

 std::vector<move_only_function<X>> mTasks; 

Then we save one of the values โ€‹โ€‹of type R once and use it again:

 template<class F, class... Args, class R=std::result_of_t<std::decay<F>_&&(std::decay_t<Args>&&...)>> std::future<R> push(F&& f, Args&&... args) { auto tuple_args=std::make_tuple(std::forward<Args>(args)...)]; // lambda will only be called once: std::packaged_task<R()> task([f=std::forward<F>(f),args=std::move(tuple_args)] return invoke_tuple( std::move(f), std::move(args) ); }); auto future = func.get_future(); // for some reason I get a compilation error in clang if I get rid of the `=, ` in this capture: mTasks.emplace_back( std::move(task) ); return future; } 

we put the arguments into the tuple, pass that tuple to the lambda, and call the tuple as "only once" in the lambda. Since we will only call the function once, we optimize the lambda for this case.

A packaged_task<R()> compatible with a move_only_function<R()> unlike a std::function<R()> , so we can just move it to our vector. std::future , which we get from it, should work fine, even if we got it before move .

This should slightly reduce overhead. Of course, there are many patterns.

I did not compile any of the codes above, I just spewed it, so the chances that everything compiles are low. But errors should mainly be tpyos.

In random order, I decided to pass move_only_function 4 different () overloads (rvalue / lvalue and const / not). I could add volatility, but that seems reckless. Which admittedly enhances the pattern.

Also in my move_only_function no operation "get at the basic stored stuff", which has std::function . Feel free to type erase if you want. And it processes (R(*)(Args...))0 , as if it were a real function pointer (I return true when I click on bool , and not as null: type erasure of convert-to- bool may be appropriate for the implementation of more industrial quality.

I rewrote std::function because std does not have a std::move_only_function , and the concept is generally useful (as evidenced by packaged_task ). Your solution makes your caller movable by wrapping it with std::shared_ptr .

If you don't like the template described above, consider writing make_copyable(F&&) , which takes an F function object and completes it using your shared_ptr method to make it copyable. You can even add SFINAE to avoid it if it can already be copied (and call it ensure_copyable ).

Then your source code will be cleaner since you just copy packaged_task and then save it.

 template<class F> auto make_function_copyable( F&& f ) { auto sp = std::make_shared<std::decay_t<F>>(std::forward<F>(f)); return [sp](auto&&...args){return (*sp)(std::forward<decltype(args)>(args)...); } } template<class F, class... Args, class R=std::result_of_t<std::decay<F>_&&(std::decay_t<Args>&&...)>> std::future<R> push(F&& f, Args&&... args) { auto tuple_args=std::make_tuple(std::forward<Args>(args)...)]; // lambda will only be called once: std::packaged_task<R()> task([f=std::forward<F>(f),args=std::move(tuple_args)] return invoke_tuple( std::move(f), std::move(args) ); }); auto future = func.get_future(); // for some reason I get a compilation error in clang if I get rid of the `=, ` in this capture: mTasks.emplace_back( make_function_copyable( std::move(task) ) ); return future; } 

it still requires the invoke_tuple template above, mainly because I don't like bind .

+6
source

Source: https://habr.com/ru/post/1212044/


All Articles