如何将通用的packaged_tasks存储在容器中?

Dav*_*vid 6 c++ multithreading c++11 c++14

我正试图采用一种"任务"的风格std::async并将其存放在容器中.我不得不跳过篮球来实现它,但我认为必须有更好的方法.

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;
}
Run Code Online (Sandbox Code Playgroud)

所以我正在使用bind- > packaged_task- > shared_ptr- > lambda- > function.我怎样才能更好/更优化?如果有一个std::function可以采取不可复制但可移动的任务,那肯定会更容易.我可以std :: forward args进入lambda的捕获,还是我必须使用bind

Yak*_*ont 6

没有像过度杀戮这样的杀戮.

第1步:编写SFINAE友好std::result_of和函数来帮助通过元组调用:

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>{};
Run Code Online (Sandbox Code Playgroud)

我们现在有invoke_result_t<A(B,C)>这是一个友好的SFINAE result_of_t<A(B,C)>can_invoke<A(B,C)>刚刚做了检查.

接下来,写一个move_only_function只移动版本的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
 };
Run Code Online (Sandbox Code Playgroud)

没有测试,只是吐了代码.在can_invoke给出了构造基本SFINAE -你可以添加"返回类型正确转换"和"void返回类型意味着我们忽略返回"如果你喜欢.

现在我们重新编写代码.首先,您的任务是仅移动功能​​,而不是功能:

std::vector<move_only_function<X>> mTasks;
Run Code Online (Sandbox Code Playgroud)

接下来,我们存储R一次类型计算,然后再次使用它:

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;
}
Run Code Online (Sandbox Code Playgroud)

我们将参数填充到元组中,将该元组传递给lambda,并在lambda中以"只执行一次"的方式调用元组.因为我们只调用一次函数,所以我们为这种情况优化lambda.

一个packaged_task<R()>是与兼容move_only_function<R()>不像std::function<R()>,所以我们就可以将其移动到我们的矢量.在std::future我们从它应该很好地工作,即使我们之前得到它move.

这应该会减少您的开销.当然,有很多样板.

我没有编译任何上面的代码,我只是吐了出来,所以编译的几率都很低.但错误应该主要是tpyos.

随机地,我决定给出move_only_function4个不同的()重载(rvalue/lvalue和const/not).我本来可以增加挥发性,但这似乎是鲁莽的.无可否认,这增加了样板.

此外,我move_only_function缺乏"获取底层存储的东西"操作std::function.如果您愿意,可以随意键入擦除.它就(R(*)(Args...))0好像它是一个真正的函数指针(我truebool转换时返回,而不是像null:转换为类型擦除 - bool对于更加工业化的质量实现可能是值得的.

我重写了std::function因为std缺少了a std::move_only_function,而且这个概念一般是有用的(如证据所示packaged_task).你的解决方案通过用一个包裹它来使你的可调用可移动std::shared_ptr.

如果你不喜欢上面的样板文件,可以考虑编写make_copyable(F&&),它接受一个函数对象F并使用你的shared_ptr技术将其包装起来以使其可复制.您甚至可以添加SFINAE,以避免在可复制(并调用它ensure_copyable)时执行此操作.

然后你的原始代码会更干净,因为你只需要packaged_task复制,然后存储它.

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;
}
Run Code Online (Sandbox Code Playgroud)

这仍然需要invoke_tuple上面的样板,主要是因为我不喜欢bind.