【问题标题】:Move-only version of std::functionstd::function 的仅移动版本
【发布时间】:2014-10-09 10:37:39
【问题描述】:

因为std::function 是可复制的,所以标准要求用于构造它的可调用对象也是可复制的:

n337 (20.8.11.2.1)

template<class F> function(F f);

要求:F 应为 CopyConstructible。 f 对于参数类型 ArgTypes 和返回类型 R 应该是可调用的 (20.8.11.2)。 A 的拷贝构造函数和析构函数不会抛出异常。`

这意味着不可能从不可复制的绑定对象或捕获诸如std::unique_ptr 之类的仅移动类型的 lambda 形成std::function

似乎可以为只移动的可调用对象实现这样的只移动包装器。 std::function 是否有标准库仅移动等效项,或者是否有解决此问题的通用解决方法?

【问题讨论】:

  • std::function 以几种不同的方式被破坏......我认为这是普遍接受的,但如果不破坏现有代码则很难修复。
  • 嘿。感谢您的评论。既然您提到了它,很高兴听到它被破坏的一些具体方式。
  • @KerrekSB 我不认为 那个 特定方面被破坏了。由于function 执行类型擦除,所以functioninstance 是否可复制将成为运行时问题。
  • 好吧,你可以reuse std::function to make it work.. 那种(注意function_mo 本身是只能移动的,所以不会抛出hack 的异常)。
  • @orm:最大的症结之一是函数调用运算符是const,库要求它表示线程安全。这使得想要在并发设置中使用function<void()> 作为通用可调用事物的人很难。另一个有点半生不熟的方面是类型擦除分配器支持,我相信(尤其是关于花哨的指针); function 是库中唯一具有类型擦除分配器并且也是可复制的类。 (某些方面见 N3916。N4041 也很有趣。)

标签: c++ c++11 move-semantics


【解决方案1】:

不,C++ std 库中没有 std::function 的仅移动版本。 (从 C++14 开始)

Fastest possible delegates 是一个类似std::function 的类的实现,它恰好比许多std 库中的大多数std::function 实现都要快,而且它应该很容易分叉成movecopy版本。

将您的仅move 函数对象包装到具有转发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) 来存储小型可调用对象(假设它们是可移动的;如果不可移动,则存储在堆上以允许移动),以及一个 get-pointer-if-you-guess -the-type-right(如std::function)。

【讨论】:

  • 您对使用“不可能的快速委托”代码的建议是一个红鲱鱼:它用来加快速度的基本技巧是特定于方法指针的(从 cmets 看起来它不是实际上无论如何都会加快速度)。如所写,它不适用于用户定义的 lambdas(或其他仿函数),这是原始问题所要问的。您的示例代码似乎更像是传统的 std::function 实现。
  • @arthur 根据我的经验,可以轻松修改代表代码以隐式使用operator()(忽略模板案例)。我不知道它是否还能提升性能;自从我在遗留情况之外使用它已经很长时间了(维护起来太痛苦了)。
  • “消费operator()”是什么意思?您的意思是使用具有operator() 的任意类(包括 lambda 函数)?如果是这样,这就是编写类型擦除仿函数的大部分工作。无论您认为它是困难还是容易,都没有必要将其链接到不做的事情(尤其是花费额外精力做不相关的事情的事情)。
  • @arthur 这是一个零分配标准函数的例子;我的更完整,但分配。
【解决方案2】:

正如其他人指出的那样,库中没有 std::function 的仅移动版本。以下是重用(滥用?)std::function 并允许它接受仅移动类型的解决方法。它很大程度上受到了 cmets 中的dyp's implementation 的启发,所以很多功劳都归功于他:

#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;
}

Working version to coliru.

【讨论】:

  • public base 允许转换为 const std :: function & 和复制(有例外)
  • @pal 转换为std::function 太奇怪了(?)。这样我们之后就可以复制std::function&lt;void()&gt; ff = f3; auto fff = ff;这怎么可能?
  • 这个实现有点缺陷:unique_function(Fn&amp;&amp; f) 与我认为的很多匹配...fn a; fn b; a = b; 编译但不应该...
  • 我只是为 unique_function&lt;T&gt; 本身禁用了该模板。
  • 应该消除存储对可调用 unique_function(Fn&amp;&amp; f) : base(wrapper&lt;std::remove_reference_t&lt;Fn&gt;&gt;{ std::forward&lt;std::remove_reference_t&lt;Fn&gt;&gt;(f) }) { } 的引用的可能性,对于 operator= 也是如此
【解决方案3】:

是的,在当前的 C++23 草案中有一个proposal for std::move_only_functionadopted 2021-10

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

另见cppreference entry on std::move_only_function

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

【讨论】:

    猜你喜欢
    • 2019-05-27
    • 1970-01-01
    • 1970-01-01
    • 2015-11-24
    • 2015-01-17
    • 2012-04-18
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多