【问题标题】:Bind move-only structure to function将只移动结构绑定到函数
【发布时间】:2015-11-19 08:37:45
【问题描述】:

我需要将结构与已删除的复制构造函数绑定到一个函数。我已将我想要实现的目标简化为以下最小示例:

struct Bar {
    int i;
    Bar() = default;
    Bar(Bar&&) = default;
    Bar(const Bar&) = delete;
    Bar& operator=(const Bar&) = delete;
};

void foo(Bar b) {
    std::cout << b.i << std::endl;
}

int main()
{
    Bar b;
    b.i = 10;

    std::function<void()> a = std::bind(foo, std::move(b)); // ERROR
    a();

    return 0;
}

从编译器中我得到的只有哀嚎和咬牙切齿:

test.cpp:22:27: error: no viable conversion from 'typename _Bind_helper<__is_socketlike<void (&)(Bar)>::value, void (&)(Bar), Bar>::type' (aka '_Bind<__func_type (typename decay<Bar>::type)>') to 'std::function<void ()>'
    std::function<void()> a = std::bind(foo, std::move(b));
                          ^   ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/usr/bin/../lib/gcc/x86_64-linux-gnu/5.1.0/../../../../include/c++/5.1.0/functional:2013:7: note: candidate constructor not viable: no known conversion from 'typename _Bind_helper<__is_socketlike<void (&)(Bar)>::value, void (&)(Bar),
      Bar>::type' (aka '_Bind<__func_type (typename decay<Bar>::type)>') to 'nullptr_t' for 1st argument
      function(nullptr_t) noexcept
      ^
/usr/bin/../lib/gcc/x86_64-linux-gnu/5.1.0/../../../../include/c++/5.1.0/functional:2024:7: note: candidate constructor not viable: no known conversion from 'typename _Bind_helper<__is_socketlike<void (&)(Bar)>::value, void (&)(Bar),
      Bar>::type' (aka '_Bind<__func_type (typename decay<Bar>::type)>') to 'const std::function<void ()> &' for 1st argument
      function(const function& __x);
      ^
/usr/bin/../lib/gcc/x86_64-linux-gnu/5.1.0/../../../../include/c++/5.1.0/functional:2033:7: note: candidate constructor not viable: no known conversion from 'typename _Bind_helper<__is_socketlike<void (&)(Bar)>::value, void (&)(Bar),
      Bar>::type' (aka '_Bind<__func_type (typename decay<Bar>::type)>') to 'std::function<void ()> &&' for 1st argument
      function(function&& __x) : _Function_base()
      ^
/usr/bin/../lib/gcc/x86_64-linux-gnu/5.1.0/../../../../include/c++/5.1.0/functional:2058:2: note: candidate template ignored: substitution failure [with _Functor = std::_Bind<void (*(Bar))(Bar)>]: no matching function for call to object of
      type 'std::_Bind<void (*(Bar))(Bar)>'
        function(_Functor);
        ^
1 error generated.

所以我想问一下是否有任何解决方法可以让我将 Bar 绑定到 foo 同时保持 Bar 只移动。

编辑: 还要考虑以下代码,其中变量b 在调用a 之前结束:

int main()
{
    std::function<void()> a;
    {
        Bar b;
        b.i = 10;
        a = std::bind(foo, std::move(b)); // ERROR
    }
    a();

    return 0;
}

【问题讨论】:

    标签: c++ c++11 stdbind


    【解决方案1】:

    std::function 不能接受只能移动的可调用对象。它会删除传入的类型以调用(使用签名)、销毁和复制1

    编写仅移动 std::function 只是一点工作。 Here is a stab at it 在不同的上下文中。 live example.

    std::packaged_task 也是一个可移动的类型橡皮擦调用器,但它的重量比你可能想要的要重,而且要取出值是一件很痛苦的事情。

    更简单的解决方案是滥用共享指针:

    template<class F>
    auto shared_function( F&& f ) {
      auto pf = std::make_shared<std::decay_t<F>>(std::forward<F>(f));
      return [pf](auto&&... args){
        return (*pf)(decltype(args)(args)...);
      };
    }
    

    将一些可调用对象包装到共享指针中,将其放入 lambda 完美转发 lambda。

    这说明了一个问题——调用不起作用!以上所有内容都有一个const 调用。

    你想要的是一个你只能调用一次的任务

    template<class Sig>
    struct task_once;
    
    namespace details_task_once {
      template<class Sig>
      struct ipimpl;
      template<class R, class...Args>
      struct ipimpl<R(Args...)> {
        virtual ~ipimpl() {}
        virtual R invoke(Args&&...args) && = 0;
      };
      template<class Sig, class F>
      struct pimpl;
      template<class R, class...Args, class F>
      struct pimpl<R(Args...), F>:ipimpl<R(Args...)> {
        F f;
        template<class Fin>
        pimpl(Fin&&fin):f(std::forward<Fin>(fin)){}
        R invoke(Args&&...args) && final override {
          return std::forward<F>(f)(std::forward<Args>(args)...);
        };
      };
      // void case, we don't care about what f returns:
      template<class...Args, class F>
      struct pimpl<void(Args...), F>:ipimpl<void(Args...)> {
        F f;
        template<class Fin>
        pimpl(Fin&&fin):f(std::forward<Fin>(fin)){}
        void invoke(Args&&...args) && final override {
          std::forward<F>(f)(std::forward<Args>(args)...);
        };
      };
    }
    template<class R, class...Args>
    struct task_once<R(Args...)> {
      task_once(task_once&&)=default;
      task_once&operator=(task_once&&)=default;
      task_once()=default;
      explicit operator bool() const { return static_cast<bool>(pimpl); }
    
      R operator()(Args...args) && {
        auto tmp = std::move(pimpl);
        return std::move(*tmp).invoke(std::forward<Args>(args)...);
      }
      // if we can be called with the signature, use this:
      template<class F,
        class R2=R,
        std::enable_if_t<
            std::is_convertible<std::result_of_t<F&&(Args...)>,R2>{}
            && !std::is_same<R2, void>{}
        >* = nullptr
      >
      task_once(F&& f):task_once(std::forward<F>(f), std::is_convertible<F&,bool>{}) {}
    
      // the case where we are a void return type, we don't
      // care what the return type of F is, just that we can call it:
      template<class F,
        class R2=R,
        class=std::result_of_t<F&&(Args...)>,
        std::enable_if_t<std::is_same<R2, void>{}>* = nullptr
      >
      task_once(F&& f):task_once(std::forward<F>(f), std::is_convertible<F&,bool>{}) {}
    
      // this helps with overload resolution in some cases:
      task_once( R(*pf)(Args...) ):task_once(pf, std::true_type{}) {}
      // = nullptr support:
      task_once( std::nullptr_t ):task_once() {}
    
    private:
      std::unique_ptr< details_task_once::ipimpl<R(Args...)> > pimpl;
    
    // build a pimpl from F.  All ctors get here, or to task() eventually:
      template<class F>
      task_once( F&& f, std::false_type /* needs a test?  No! */ ):
        pimpl( new details_task_once::pimpl<R(Args...), std::decay_t<F>>{ std::forward<F>(f) } )
      {}
      // cast incoming to bool, if it works, construct, otherwise
      // we should be empty:
      // move-constructs, because we need to run-time dispatch between two ctors.
      // if we pass the test, dispatch to task(?, false_type) (no test needed)
      // if we fail the test, dispatch to task() (empty task).
      template<class F>
      task_once( F&& f, std::true_type /* needs a test?  Yes! */ ):
        task_once( f?task_once( std::forward<F>(f), std::false_type{} ):task_once() )
      {}
    };
    

    live example.

    请注意,您只能在具有上述task_once 的右值上下文中调用()。这是因为() 具有破坏性,而您的情况应该如此。

    遗憾的是,以上依赖于 C++14。而且我现在不喜欢编写 C++11 代码。所以,这里有一个更简单但性能较低的 C++11 解决方案:

    std::function<void()> a;
    {
        Bar b;
        b.i = 10;
        auto pb = std::make_shared<Bar>(std::move(b));
        a = [pb]{ return foo(std::move(*pb)); };
    }
    a();
    

    这会将b 的移动副本推送到共享指针中,将其存储在std::function 中,然后在您第一次调用() 时破坏性地使用它。


    1 没有它就实现move(除非它使用小函数优化,我希望它使用类型的move)。它还实现了转换回原始类型,但每种类型都支持这一点。对于某些类型,它支持 check-for-null(即,显式转换为 bool),但老实说,我不确定它这样做的确切类型。

    【讨论】:

    • 哇,好吧,看起来很复杂。但是您的解决方案会处理我在编辑中发布的代码吗?
    • @Jendas 抱歉,task_once 遇到了 void 返回类型的问题。已修复:查看新的实时示例。只需将 std::function 替换为 task_once,并使用移动捕获可变 lambdas 而不是 std::bindstd::bind 很少是个好主意。
    • @Yakk:我在您的编辑中提出了与您的 C++11 类似的解决方案。我说你只能调用一次 std::function 是否正确,此时它已经消耗了你的对象,你不应该再次调用它?
    • @AndyG 如果你第二次调用它(或第二次调用它的副本),你最终会在一个移动的对象上调用它。这可能是也可能不是您想要发生的事情。在每种情况下,OP 的函数对象只能被调用一次(在调用已移动对象之前),因为我们有一个仅移动捕获的值调用一个按值获取其参数的函数。
    【解决方案2】:

    您可以使用指针、lambda 和 std::bind 的组合来绕过 std::functionCopyConstructible 约束:

    auto lambda = [](Bar* b){::foo(std::move(*b));};
    std::function<void()> a = std::bind(lambda, &b);
    a();
    

    Example


    编辑

    在 C++11 中使用 lambda 和按引用捕获的单行代码

    std::function<void()> a = [&b](){::foo(std::move(b));};
    a()
    

    Example2

    编辑2

    (将评论移到我的答案中)

    在您的代码编辑添加了函数对象应该能够超过绑定到函数的变量范围的约束之后,我们仍然可以使用 lambda 来完成此操作,只是现在我们应该捕获一个 shared_ptr使用分配和移动构造来保存Bar

    在下面的示例中,我使用 C++14 的通用捕获来捕获 shared_ptr。 @Yakk's solution 将其翻译成 C++11。

    std::function<void()> a;
    {
        Bar b;
        b.i = 10;
        a = [b2 = std::make_shared<decltype(b)>(std::move(b))]()
        {
            // move the underlying object out from under b2
            // which means b2 is in a valid but undefined state afterwards
            ::foo(std::move(*b2));
        }; 
    }
    

    Example3

    【讨论】:

    • 感谢您的快速回答,但我怀疑这种方法是否等同于使用 bind。如果我错了,请纠正我,但如果在a() 之前调用析构函数,代码将导致未定义的行为或类似的东西。这当然不是我需要的。
    • 我将您的代码缩减为 - coliru.stacked-crooked.com/a/62d45f89ae2911bc,这看起来非常符合我的需要。但它是如何工作的呢?
    • @Jendas:对不起,请不要那样做。移动构造在调用函数之前不会发生,此时您的变量已经被破坏,因此它可能导致未定义的行为。
    • 是的,我是这么认为的:-/ 看起来不错,但在未定义的行为区域中非常多。那个sux。
    • @Jendas:我认为这会做你想做的事,但你只能调用你的函数一次! coliru.stacked-crooked.com/a/c98b9e2b1071a916
    猜你喜欢
    • 1970-01-01
    • 2021-10-01
    • 1970-01-01
    • 2011-10-01
    • 2021-04-24
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多