How can I store generic packaged_tasks in a container?

前端 未结 1 1189
傲寒
傲寒 2021-02-06 11:45

I\'m trying to take a \'task\' in the style of std::async and store it in a container. I\'m having to jump through hoops to achieve it, but I think there must be a

1条回答
  •  孤街浪徒
    2021-02-06 12:32

    There is no kill like overkill.

    Step 1: write a SFINAE friendly std::result_of and a function to help calling via tuple:

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

    We now have invoke_result_t which is a SFINAE friendly result_of_t and can_invoke which just does the check.

    Next, write a move_only_function, a move-only version of std::function:

    namespace details {
      template
      struct mof_internal;
      template
      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
      struct mof_pimpl;
      template
      struct mof_pimpl:mof_internal {
        F f;
        virtual R invoke( Args&&... args ) const&  override { return f( std::forward(args)... ); }
        virtual R invoke( Args&&... args )      &  override { return f( std::forward(args)... ); }
        virtual R invoke( Args&&... args ) const&& override { return std::move(f)( std::forward(args)... ); }
        virtual R invoke( Args&&... args )      && override { return std::move(f)( std::forward(args)... ); }
      };
    }
    
    template
    struct move_only_function {
      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)...); }
      R operator()(Args...args)const& { return                  pImpl().invoke(std::forward(args)...); }
      R operator()(Args...args)     &&{ return std::move(*this).pImpl().invoke(std::forward(args)...); }
      R operator()(Args...args)const&&{ return std::move(*this).pImpl().invoke(std::forward(args)...); }
    
      template(Args...)>>
      move_only_function(F&& f):
        m_pImpl( std::make_unique, R(Args...)>>( std::forward(f) ) )
      {}
    private:
      using internal = details::mof_internal;
      std::unique_ptr 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 the code. The can_invoke gives the constructor basic SFINAE -- you can add "return type converts properly" and "void return type means we ignore the return" if you like.

    Now we rework your code. First, your task are move-only functions, not functions:

    std::vector> mTasks;
    

    Next, we store the R type calculation once, and use it again:

    template_&&(std::decay_t&&...)>>
    std::future
    push(F&& f, Args&&... args)
    {
      auto tuple_args=std::make_tuple(std::forward(args)...)];
    
      // lambda will only be called once:
      std::packaged_task task([f=std::forward(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 stuff the arguments into a tuple, pass that tuple into a lambda, and invoke the tuple in a "only do this once" kind of way within the lambda. As we will only invoke the function once, we optimize the lambda for that case.

    A packaged_task is compatible with a move_only_function unlike a std::function, so we can just move it into our vector. The std::future we get from it should work fine even though we got it before the move.

    This should reduce your overhead by a bit. Of course, there is lots of boilerplate.

    I have not compiled any of the above code, I just spewed it out, so the odds it all compiles are low. But the errors should mostly be tpyos.

    Randomly, I decided to give move_only_function 4 different () overloads (rvalue/lvalue and const/not). I could have added volatile, but that seems reckless. Which increase boilerplate, admittedly.

    Also my move_only_function lacks the "get at the underlying stored stuff" operation that std::function has. Feel free to type erase that if you like. And it treats (R(*)(Args...))0 as if it was a real function pointer (I return true when cast to bool, not like null: type erasure of convert-to-bool might be worthwhile for a more industrial quality implementation.

    I rewrote std::function because std lacks a std::move_only_function, and the concept in general is a useful one (as evidenced by packaged_task). Your solution makes your callable movable by wrapping it with a std::shared_ptr.

    If you don't like the above boilerplate, consider writing make_copyable(F&&), which takes an function object F and wraps it up using your shared_ptr technique to make it copyable. You can even add SFINAE to avoid doing it if it is already copyable (and call it ensure_copyable).

    Then your original code would be cleaner, as you'd just make the packaged_task copyable, then store that.

    template
    auto make_function_copyable( F&& f ) {
      auto sp = std::make_shared>(std::forward(f));
      return [sp](auto&&...args){return (*sp)(std::forward(args)...); }
    }
    template_&&(std::decay_t&&...)>>
    std::future
    push(F&& f, Args&&... args)
    {
      auto tuple_args=std::make_tuple(std::forward(args)...)];
    
      // lambda will only be called once:
      std::packaged_task task([f=std::forward(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;
    }
    

    this still requires the invoke_tuple boilerplate above, mainly because I dislike bind.

    0 讨论(0)
提交回复
热议问题