std::function的移动语义版本

70
因为 std::function 可以复制,所以标准要求用于构造它的可调用对象也必须可以被复制: n337 (20.8.11.2.1)

template<class F> function(F f);

要求: F 必须是可复制的。对于参数类型 ArgTypes 和返回类型 R 的可调用对象 f 应该是可调用的(20.8.11.2)。A 的复制构造函数和析构函数不得抛出异常。

这意味着无法从不可复制的绑定对象或捕获了移动语义类型(例如 std::unique_ptr)的 lambda 创建 std::function。 似乎可以实现这样的仅支持移动的移动可调用对象包装器。是否有标准库中的仅支持移动的等效替代品或此问题的常见解决方案?

9
std::function 在很多方面都存在问题,这个观点被普遍认可,但是要修复它却非常困难,因为这会破坏现有的代码。 - Kerrek SB
12
嗨,谢谢您的评论。既然您提到了这个问题,能否具体说明一些它存在的问题呢? - orm
6
我不认为“那个”特定方面是有问题的。由于function执行类型擦除,因此是否可复制取决于该function实例在运行时是否可复制。 - dyp
4
其中一个主要的困难点是函数调用运算符被声明为const,而库要求这意味着线程安全。这使得那些想在并发环境中使用function<void()>作为通用可调用对象的人们感到困难。另一个有些不成熟的方面是类型抹除分配器支持,特别是涉及花哨指针的部分;function是该库中唯一具有类型抹除分配器且可复制的类。(有关一些方面,请参阅N3916。N4041也很有趣。) - Kerrek SB
4
有一个提案关于 std::any_invocable(有时称为 unique_function),已经获得批准,但它不会出现在 C++20 中。 - Davis Herring
显示剩余11条评论
3个回答

19
不,C++的std库中没有std::function的仅移动版本。(截至C++14) Fastest possible delegates是一个类似于std::function的实现,它比许多std库中的std::function实现更快,并且应该很容易分叉成一个movecopy版本。
将您的仅移动函数对象包装到具有转发operator()的类中的shared_ptr<F>中是另一种方法。
这是一个task草图:
template<class Sig>
struct task;

namespace details {
  template<class Sig>
  struct task_iimpl;
  template<class R, class...Args>
  struct task_iimpl<R(Args...)> {
    virtual ~task_iimpl() {}
    virtual R invoke(Args&&...args) const = 0;
  };
  template<class F, class Sig>
  struct task_impl;
  template<class F, class R, class...Args>
  struct task_impl<F,R(Args...)>:
    task_iimpl<R(Args...)>
  {
    F f;
    template<class T>
    task_impl(T&& t):f(std::forward<T>(t)) {}
    virtual R invoke(Args&&...args) const override {
      return f( std::forward<Args>(args...) );
    }
  };
  template<class F, class...Args>
  struct task_impl<F,void(Args...)>:
    task_iimpl<void(Args...)>
  {
    F f;
    template<class T>
    task_impl(T&& t):f(std::forward<T>(t)) {}
    virtual void invoke(Args&&...args) const override {
      f( std::forward<Args>(args...) );
    }
  };
}
template<class R, class...Args>
struct task<R(Args...)> {
  virtual ~task_iimpl() {}
  R operator()(Args...args) const {
    return pImpl->invoke(std::forward<Args>(args...));
  }
  explicit operator bool()const{ return static_cast<bool>(pImpl); }
  task(task &&)=default;
  task& operator=(task &&)=default;
  task()=default;

  // and now for a mess of constructors
  // the rule is that a task can be constructed from anything
  // callable<R(Args...)>, destroyable, and can be constructed
  // from whatever is passed in.  The callable feature is tested for
  // in addition, if constructed from something convertible to `bool`,
  // then if that test fails we construct an empty task.  This makes us work
  // well with empty std::functions and function pointers and other tasks
  // that are call-compatible, but not exactly the same:
  struct from_func_t {};
  template<class F,
    class dF=std::decay_t<F>,
    class=std::enable_if_t<!std::is_same<dF, task>{}>,
    class FR=decltype(std::declval<F const&>()(std::declval<Args>()...)),
    std::enable_if_t<std::is_same<R, void>{} || std::is_convertible<FR, R>{} >*=0,
    std::enable_if_t<std::is_convertible<dF, bool>{}>*=0
  >
  task(F&& f):
    task(
      static_cast<bool>(f)?
      task( from_func_t{}, std::forward<F>(f) ):
      task()
    )
  {}
  template<class F,
    class dF=std::decay_t<F>,
    class=std::enable_if_t<!std::is_same<dF, task>{}>,
    class FR=decltype(std::declval<F const&>()(std::declval<Args>()...)),
    std::enable_if_t<std::is_same<R, void>{} || std::is_convertible<FR, R>{} >*=0,
    std::enable_if_t<!std::is_convertible<dF, bool>{}>*=0
  >
  task(F&& f):
    task( from_func_t{}, std::forward<F>(f) )
  {}

  task(std::nullptr_t):task() {}
  // overload resolution helper when signatures match exactly:
  task( R(*pf)(Args...) ):
    task( pf?task( from_func_t{}, pf ):task() )
  {}
private:
  template<class F,
    class dF=std::decay_t<F>
  >
  task(from_func_t, F&& f):
    pImpl( std::make_unique<details::task_impl<dF,R(Args...)>>(
      std::forward<F>(f)
    )
  {}

  std::unique_ptr<details::task_iimpl<R(Args...)> pImpl;
};

但是它还没有经过测试或编译,我只是写了下来。
更具工业强度的版本将包括小缓冲优化(SBO),用于存储小型可调用对象(假设它们是可移动的;如果不可移动,则存储在堆上以允许移动),以及一个获取指针(如果你猜对了类型)的函数(例如std::function)。

2
你建议使用“不可能快速委托”代码是一个误导:它用于加速的关键技巧是特定于方法指针的(从评论中看,似乎实际上也不能加速)。按照现有的编写方式,它无法处理用户定义的lambda表达式(或其他函数对象),而这正是原始问题所询问的。你的示例代码似乎更像是传统的std::function实现。 - Arthur Tacca
@arthur 委托代码可以很容易地进行修改,以便在我的经验中隐式地使用operator()(忽略模板情况)。 我不知道它是否仍然能够提高性能;自从我在非遗留情况下使用它以来已经过了很长时间(维护起来太麻烦了)。 - Yakk - Adam Nevraumont
您所说的“consume operator()”是什么意思?您是否指消费具有 operator() 的任意类(包括 Lambda 函数)?如果是这样,那就是编写类型擦除函数对象的大部分工作。无论您认为它难还是易,都没有必要链接到无法完成此操作的内容(特别是花费额外精力执行无关操作的内容)。 - Arthur Tacca
@arthur 这是一个零分配的标准函数示例,我的更完整但会分配内存。 - Yakk - Adam Nevraumont

13

是的,在当前的C++23草案中有std::move_only_function的提案,该提案已于2021年10月通过:

本文提出了一个保守的、仅限移动的std::function等效物。

另请参阅std::move_only_function的cppreference条目

std::move_only_function类模板是一个通用的多态函数包装器。 std::move_only_function对象可以存储和调用任何可构造(不要求是可移动构造)的可调用目标——函数、lambda表达式、绑定表达式或其他函数对象,以及成员函数指针和成员对象指针。
...
std::move_only_function满足可移动构造可移动赋值的要求,但不满足可复制构造可复制赋值的要求。


12

正如其他人指出的那样,库中没有std::function的仅移动版本。以下是一种解决方法,它重用(滥用?)std::function并允许其接受仅移动类型。这在很大程度上受到dyp的实现评论中的启示,因此很多功劳归于他:

#include <functional>
#include <iostream>
#include <type_traits>
#include <utility>

template<typename T>
class unique_function : public std::function<T>
{
    template<typename Fn, typename En = void>
    struct wrapper;

    // specialization for CopyConstructible Fn
    template<typename Fn>
    struct wrapper<Fn, std::enable_if_t< std::is_copy_constructible<Fn>::value >>
    {
        Fn fn;

        template<typename... Args>
        auto operator()(Args&&... args) { return fn(std::forward<Args>(args)...); }
    };

    // specialization for MoveConstructible-only Fn
    template<typename Fn>
    struct wrapper<Fn, std::enable_if_t< !std::is_copy_constructible<Fn>::value
        && std::is_move_constructible<Fn>::value >>
    {
        Fn fn;

        wrapper(Fn&& fn) : fn(std::forward<Fn>(fn)) { }

        wrapper(wrapper&&) = default;
        wrapper& operator=(wrapper&&) = default;

        // these two functions are instantiated by std::function
        // and are never called
        wrapper(const wrapper& rhs) : fn(const_cast<Fn&&>(rhs.fn)) { throw 0; } // hack to initialize fn for non-DefaultContructible types
        wrapper& operator=(wrapper&) { throw 0; }

        template<typename... Args>
        auto operator()(Args&&... args) { return fn(std::forward<Args>(args)...); }
    };

    using base = std::function<T>;

public:
    unique_function() noexcept = default;
    unique_function(std::nullptr_t) noexcept : base(nullptr) { }

    template<typename Fn>
    unique_function(Fn&& f) : base(wrapper<Fn>{ std::forward<Fn>(f) }) { }

    unique_function(unique_function&&) = default;
    unique_function& operator=(unique_function&&) = default;

    unique_function& operator=(std::nullptr_t) { base::operator=(nullptr); return *this; }

    template<typename Fn>
    unique_function& operator=(Fn&& f)
    { base::operator=(wrapper<Fn>{ std::forward<Fn>(f) }); return *this; }

    using base::operator();
};

using std::cout; using std::endl;

struct move_only
{
    move_only(std::size_t) { }

    move_only(move_only&&) = default;
    move_only& operator=(move_only&&) = default;

    move_only(move_only const&) = delete;
    move_only& operator=(move_only const&) = delete;

    void operator()() { cout << "move_only" << endl; }
};

int main()
{
    using fn = unique_function<void()>;

    fn f0;
    fn f1 { nullptr };
    fn f2 { [](){ cout << "f2" << endl; } }; f2();
    fn f3 { move_only(42) }; f3();
    fn f4 { std::move(f2) }; f4();

    f0 = std::move(f3); f0();
    f0 = nullptr;
    f2 = [](){ cout << "new f2" << endl; }; f2();
    f3 = move_only(69); f3();

    return 0;
}

在 coliru 上的工作版本


公共基类允许转换为const std::function&并进行复制(带有异常)。 - pal
@pal 将其转换为 std::function 真的很奇怪(?). 以这种方式,我们可以随后复制 std::function<void()> ff = f3; auto fff = ff; 这是如何可能的? - Gabriel
这个实现有些缺陷:unique_function(Fn&& f) 匹配太多了,我认为...fn a; fn b; a = b; 能编译通过但不应该... - Gabriel
我只是禁用了unique_function<T>本身的那个模板。 - WolleTD
应该删除存储可调用对象的引用的可能性,unique_function(Fn&& f) : base(wrapper<std::remove_reference_t<Fn>>{ std::forward<std::remove_reference_t<Fn>>(f) }) { },以及operator= - Ricky Lung
@RickyLung 随意编辑答案。已经过了几年,我需要一段时间来重新适应它。 - Super-intelligent Shade

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