【问题标题】:How can I store generic packaged_tasks in a container?如何将通用 packaged_tasks 存储在容器中?
【发布时间】:2015-03-26 14:51:15
【问题描述】:

我正在尝试以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;
}

所以我使用bind -> packaged_task -> shared_ptr -> lambda -> function。我怎样才能更好/更优化地做到这一点?如果有一个std::function 可以执行不可复制但可移动的任务,那肯定会更容易。我可以 std::forward args 捕获 lambda,还是必须使用 bind

【问题讨论】:

  • 为什么不直接把打包好的task放到vector中呢?
  • @KerrekSB 因为packaged_tasks 是不可复制的,而std::function 只接受可复制的东西。
  • 我的意思是std::vector&lt;std::packaged_task&lt;void()&gt;&gt;
  • @KerrekSB 不会是void(),而是R(),其中R 取决于传入的任务。我希望packaged_task 是@987654337 @ 实际上,但我无法让 lambda 正确捕获 args,所以我改为使用 bind

标签: c++ multithreading c++11 c++14


【解决方案1】:

没有比杀戮更过分的杀戮。

第 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>{};

我们现在有invoke_result_t&lt;A(B,C)&gt;,这是一个对 SFINAE 友好的 result_of_t&lt;A(B,C)&gt;can_invoke&lt;A(B,C)&gt;,它只是进行检查。

接下来,写一个move_only_functionstd::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
 };

未经测试,只是喷出代码。 can_invoke 为构造函数提供了基本的 SFINAE - 如果您愿意,您可以添加“返回类型正确转换”和“void 返回类型意味着我们忽略返回”。

现在我们重新编写您的代码。首先,您的任务是只能移动的函数,而不是函数:

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

接下来,我们将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;
}

我们将参数填充到一个元组中,将该元组传递给一个 lambda,并在 lambda 中以“仅执行一次”的方式调用该元组。由于我们只会调用一次函数,因此我们针对这种情况优化了 lambda。

packaged_task&lt;R()&gt;move_only_function&lt;R()&gt; 兼容,与 std::function&lt;R()&gt; 不同,因此我们可以将其移动到向量中。我们从中获得的std::future 应该可以正常工作,即使我们在move 之前获得它。

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

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

随机地,我决定给move_only_function 4 个不同的() 重载(rvalue/lvalue 和 const/not)。我本可以添加 volatile,但这似乎是鲁莽的。诚然,这增加了样板。

我的move_only_function 也缺少std::function 具有的“获取底层存储的东西”操作。如果您愿意,请随意键入擦除。并且它将(R(*)(Args...))0 视为一个真正的函数指针(我在转换为bool 时返回true,而不是像null:convert-to-bool 的类型擦除对于更工业质量的实现可能是值得的.

我重写了std::function,因为std 缺少std::move_only_function,而且这个概念通常是有用的(packaged_task 证明了这一点)。您的解决方案通过使用 std::shared_ptr 包装可移动可调用对象。

如果您不喜欢上面的样板,请考虑编写make_copyable(F&amp;&amp;),它采用函数对象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;
}

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

【讨论】:

  • 所以 2 个根本问题是没有 move_only_function 并且您不能将可变参数转发到 lambda 捕获中。您编写了 move_only_function 并使用 tuple 将可变参数转发到 lambda 捕获中。触摸。但是,tuple 是否比使用bind 更优化/更好,因为bind 的样板要少得多。此外,还有我的简单标题类,包含所有这些东西; C++ 应该支持这两个问题!
  • @Dave 是的。一件好事是move_only_function(或任何您想称呼它的名称)可用于其他目的,您可以获取“最快的委托”以使其比大多数std::function 实现更快。是的,bind 做的事情与我的tuple 跳舞非常相似,但我不喜欢bind(它有太多的魔力——绑定一个绑定并撕掉你的头发),invoke_tuple 很简单( 6-10 行代码?)感觉更干净。
  • 考虑使用bind并减少一些样板(但仍然使用move_only_function,如果我使用packaged_task而不是@987654376,它将是:mTasks.push_back(std::bind(std::move(func), std::forward&lt;Args&gt;(args)...)); @.
  • @Dave 将packaged_task&lt;R(args...)&gt; 存储在bind 中,然后存储在std::vector&lt;packaged_task&lt;void()&gt;&gt; 中。是的,有点疯狂,但它可能会起作用,并摆脱shared_ptr
  • 所以只使用packaged_task 作为move_only_function 之类的?哎呀
猜你喜欢
  • 1970-01-01
  • 2014-11-08
  • 1970-01-01
  • 2011-04-29
  • 2021-12-15
  • 1970-01-01
  • 1970-01-01
  • 2011-01-17
  • 1970-01-01
相关资源
最近更新 更多