将移动语义结构绑定到函数

15

我需要将一个被删除复制构造函数的结构体绑定到一个函数上。我已经将我尝试实现的内容简化为以下最小示例:

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;
}
2个回答

7

std::function 无法接受移动语义的可调用对象。它会将传入的类型擦除为调用(带有签名)、销毁和复制1

编写一个只支持移动的 std::function 需要一些工作量。在这里可以找到一个不同上下文中的尝试。可以在此处查看实时示例。

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中。

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

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

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() )
  {}
};

请注意,您只能在上述任务中的rvalue上下文中调用()。这是因为()是具有破坏性的,在您的情况下应该是这样的。

不幸的是,上述代码仅适用于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 它实现了移动操作(除非使用小型函数优化,在这种情况下希望使用类型的移动)。它还实现了转换回原始类型,但每种类型都支持这种操作。对于某些类型,它支持检查是否为空(即显式转换为bool),但我对它确切支持的类型并不确定。


哇,好的,看起来很复杂。但是你的解决方案能处理我在编辑中发布的代码吗? - Jendas
1
@Jendas 抱歉,task_once 函数的 void 返回类型出现了问题。已经修复:请查看新的实时示例。只需将 std::function 替换为 task_once,并使用可移动捕获的可变 lambda 表达式代替 std::bind。通常情况下,std::bind 不是一个好主意。 - Yakk - Adam Nevraumont
@Yakk:我想出了一个类似于你在C++11中编辑的解决方案。我说得对吗?你只能调用std::function一次,此时它已经使用了你的对象,你不应该再次调用它,是吗? - AndyG
@AndyG 如果您第二次调用它(或者第二次调用它的副本),您将会在一个已移动的对象上调用它。这可能是您想要发生的,也可能不是。在任何情况下,OP的函数对象只能被调用一次(在对已移动的对象进行调用之前),因为我们有一个仅限移动捕获值调用一个按值传递其参数的函数。 - Yakk - Adam Nevraumont

2
您可以通过使用指针、lambda和std::bind的组合来绕过std::function的CopyConstructible约束:
auto lambda = [](Bar* b){::foo(std::move(*b));};
std::function<void()> a = std::bind(lambda, &b);
a();

示例


编辑

C++11中使用lambda和引用捕获的一行代码

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

Example2

Edit2

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

根据您添加的代码编辑,该约束条件要求函数对象能够超出绑定到函数的变量的范围,我们仍然可以使用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));
    }; 
}

{{链接1:示例3}}


我很感激您的快速回答,但我怀疑这种方法是否等同于使用bind。如果我错了,请纠正我,但如果在调用a()之前某些东西会调用析构函数,那么代码将导致未定义的行为或类似的问题。这绝对不是我需要的任何东西。 - Jendas
我简化了你的代码 - http://coliru.stacked-crooked.com/a/62d45f89ae2911bc,看起来非常符合我的需求。但是它是如何工作的? - Jendas
@Jendas:抱歉,请不要这样做。移动构造函数直到调用函数时才会发生,此时您的变量已经被销毁,因此可能会导致未定义的行为。 - AndyG
是的,我也这么想 :-/ 它看起来还可以,但非常容易出现未定义行为。真糟糕。 - Jendas
@Jendas:我认为这可以实现你想要的功能,但是你只能调用你的函数一次!http://coliru.stacked-crooked.com/a/c98b9e2b1071a916 - AndyG
@Jendas:我应该指出,链接中的代码是C++14的。如果需要C++11等效版本,请参考Yakk的编辑。 - AndyG

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