现代C++中std::bind的替代方案

10

我正在尝试创建一个类,其中包含不同类型的函数对象的容器。

这是它的简化版本。

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...));

    //.....
}

目前还有一些问题我还无法解决。

  1. 有没有一种方法可以移除std::bind并存储一个不同的对象或指针?
  2. 这能更加通用吗?我是否可以以某种方式将返回不同对象的函数存储在单个容器(如intvoid…)中?
  3. 是否可以在编译时执行创建任务的某些逻辑?类似于consexpr绑定。

3
一个 lambda 函数可以替换调用 bind 函数。 - StoryTeller - Unslander Monica
4
但是std::bind就是旧的std::bind2nd的现代等价物。 std :: bind不再很酷了吗? 说真的,好问题。 - Bathsheba
而且你的容器已经可以容纳任何满足 T t = func(); 编译的函数对象。 - StoryTeller - Unslander Monica
bind looks fine here. beats writing container.emplace_back([func, args...](){ return func(args...); }); - NathanOliver
@PetarVelev - 那并没有真正回答我的问题。如果容器持有一个返回值的函数对象,在调用之后这个返回值会去哪里?你是说你不会丢弃返回值吗?你会将其存储并放在某个地方吗? - StoryTeller - Unslander Monica
显示剩余9条评论
2个回答

7

来自原始评论的内容。

有的。这是简化的。我在实际代码中使用 futures 和一个特殊的容器。它旨在用于多线程环境中。

这被称为隐藏主题。

如果您正在存储可调用对象以在其他线程中调用,在另一个线程中,您需要签名 void()。在 线程中,您需要填充一个 std::future

至于绑定参数,虽然很多 std 函数会为您执行此操作,但我发现最好要求具有预绑定参数的可调用对象。他们可以在外部使用 std::bind、lambda 或任何其他他们选择的方法来完成这项任务。

所以就是这样。

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;

加入一些互斥锁并摇晃即可。

在这里,我使用 std::packaged_task<void()> 作为预先编写的移动封装容器,用于任何具有该签名的内容。我们不使用它可以产生的 future,这是一种浪费,但它比编写自己的移动仅拥有一次的函数对象更短。

我个人只是编写了一个轻量级的移动封装类似于 std::function<void()>,而不是使用 std::packaged_task<void()>,但这可能是不明智的。

addTask 返回的 future 在调用 packaged_task<R()> 时得到满足,当调用 packaged_task<void()> 时调用它(可能在另一个线程中)。


结构之外,调用者可以提供任何零参数可调用对象。

99 次中有 100 次,简单的 [some_arg]{ some_code; } 或甚至 []{ some_code; } 即可工作。在复杂情况下,它们可以使用 std::bind 或 C++14 中更复杂的 lambda 来处理。

将参数的存储放入 addTask 中混合了线程任务队列的责任和处理参数的责任。

实际上,我会单独编写一个线程安全的队列,然后让线程池使用它:

template<class T>
struct thread_safe_queue;

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

在C++17中,替代bind的函数如下所示:
[
  func = std::forward<Func>(func),
  args = std::make_tuple( std::forward<Args>(args)... )
]() mutable {
  std::apply( func, std::move(args) );
}

在C++14中,你可以很容易地编写notstd :: apply。 如果你需要有效地移动参数,你需要使用std绑定或在C++11中手动创建函数对象。
我会主张将参数绑定强烈放在使用线程池的代码领域内是最好的选择。
这也允许线程池执行像传递任务可选的额外参数,比如“取消令牌”之类的操作。

这是一个很棒的答案,谢谢。这是我第二次遇到 std::decay,第一次没有尝试调查它。另外,将类型为 intstd::packaged_task 移动到类型为 void 的任务中是否可以?我还考虑过线程安全队列,但我已经花了很多精力使同步最小化,并不确定线程安全队列是否会影响性能。 - Petar Velev
1
@PetarVelev 一个 packaged_task<R()> 可以使用签名 void() 调用,无论 R 是什么,因为它的 R 被“重定向”到 future<R> get_future() 返回。一个 packaged_task<void()> 可以存储任何具有签名 void() 的内容。因此,一个 packaged_task<R()> 可以存储在 packaged_task<void()> 中。对于队列来说,“最小同步”必须放在某个地方。我声明这个同步放在它自己的类型(线程安全队列)中,并且线程池使用该队列公开的任何良好定义的接口。单一职责原则。 - Yakk - Adam Nevraumont
关于这个线程安全队列...假设有一个deque方法 - 我该如何确保对象只在future返回后被销毁。还有一件事让我担心 - 池中有一个std::atomic_bool stop;,它指示池应该何时停止并销毁自身。如果同步在队列中,我认为必须再加一个互斥锁来处理这个变量。 - Petar Velev
我的意思是,如果我从队列中删除任务并将其传递给线程,那么该任务将被删除,程序将失败。我需要在线程完成任务后才能将任务出队。 - Petar Velev
1
@ChrisGuzak 已修复,还添加了一个缺失的 const - Yakk - Adam Nevraumont
显示剩余3条评论

7

std::bind源自于boost::bind,在我们拥有lambda之前是必要的。

不幸的是,在c++标准中,std::bind和lambda同时出现,因此它几乎立即变得不相关了。

在c++14及以上版本中,您可以使用可变参数lambda捕获函数和参数:

template<class T>
template<typename F, typename ... ARGS>
T Container<T>::addTask(F && func, ARGS && ... args)
{
    container.emplace_back( [func = std::forward<F>(func),
                             args...] 
                             ()  mutable // make mutable if you want to move the args in to func
                             {
                                 return func(std::move(args)...);
                             });

    //.....
}

您无法通过这种方式完全实现完美转发。在args...的捕获中存在一个隐含的复制。

这在c++17中得到解决。

template<class T>
template<typename F, typename ... ARGS>
T Container<T>::addTask(F && func, ARGS && ... args)
{
    container.emplace_back( [func = std::forward<F>(func),
                             args = std::make_tuple(std::forward<ARGS>(args)...)                                 ] 
                             ()  mutable // make mutable if you want to move the args in to func
                             {
                                 return std::apply(func, std::move(args));
                             });

    //.....
}

你的C++17示例正在以引用的方式捕获args。 - Barry

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接