std::bind和std::thread中的移动语义/行为

3

请看下面的简单测试程序,您只需复制并进行测试。我已经使用了gcc 4.9来编译它,编译正常。

#include <iostream>
#include <functional>
#include <thread>
#include <string>

class Test
{
public:
  Test(const Test &t) { this->name = t.name; std::cout << name << ": copy constructor" << std::endl; }
  Test(Test &&t) {this->name = std::move(t.name); std::cout << name << ": move contructor" << std::endl; }
  Test(const std::string &name) {this->name=name;}

  Test &operator=(const Test &t) {  this->name = t.name; std::cout << name << ": copy operator = " << std::endl; return  *this; }
  Test &operator=(Test &&t) { this->name = std::move(t.name); std::cout << name << ": move operator = " << std::endl; return *this; }

  std::string name;
};


class A
{
public:
  void f(Test t1, Test t2)
  {
    std::cout << "running f" << std::endl;
  }
  void run()
  {
    std::cout << "running run" << std::endl;
    Test t1("t1");
    Test t2("t2");
    auto functor = std::bind(&A::f, this, t1, std::placeholders::_1);
    std::cout << "functor created by bind, t1 is passed into functor" << std::endl;

    std::thread t(functor, t2);

    std::cout << "thread created, functor and t2 passed into thread" << std::endl;
    t.join();
  }
};

int main()
{
  A a;
  a.run();

  return 0;
}

程序在gcc 4.9 (mingw)下的输出如下:

运行 run

t1:复制构造函数

由bind创建的函数对象,t1被传入函数对象

t2:复制构造函数

t1:复制构造函数

t2:移动构造函数

t1:移动构造函数

线程已创建,函数对象和t2被传入线程

t2:移动构造函数

t1:复制构造函数

运行 f

请注意粗体字。
(1)我想知道为什么在将函数对象和t2传入线程之前会有t2 movet1 move
(2)以及为什么在调用f()之前会有t2 MOVEt1 COPY
gcc的库实现是否进行了某些优化以将COPY转换为MOVE以提高效率?例如,在调用f(Test t1, Test t2)之前,将t2移动到其中?
如果我将上述两行更改为:
auto functor = std::bind(&A::f, this, std::move(t1), std::placeholders::_1);
std::thread t(std::move(functor), std::move(t2));

接下来的一切都会被移动,除了最后一个“t1 copy”。

(3) 为什么t1还是被复制了?这与(2)有关。

如果我再改变一行,

void f(Test &t1, Test &t2)

然后它无法编译。

(4) std::bindstd::thread的内部实现不是存储左值对象t1和t2吗?为什么调用Test会失败?我很好奇标准是怎么说的。

如果我把它改成:

void f(const Test &t1, const Test &t2)

一切都正常工作,最后两个t2移动和t1复制被消除了。

(5)我只是想让有人与我确认这是否有效,并且即使我们将线程t存储在其他地方,也不会出现悬空引用的危险。例如,以下内容仍然有效吗?

class A
{
public:
  void f(const Test &t1, const Test &t2)
  {
    std::cout << "running f" << std::endl;
  }
  void run()
  {
    std::cout << "running run" << std::endl;
    Test t1("t1");
    Test t2("t2");
    auto functor = std::bind(&A::f, this, std::move(t1), std::placeholders::_1);
    std::cout << "functor created by bind, t1 is passed into functor" << std::endl;

    std::thread t(std::move(functor), std::move(t2));
    std::cout << "thread created, functor and t2 passed into thread" << std::endl;

    t_internal.swap(t);
  }

  std::thread t_internal;
};

int main()
{
  A a;
  a.run();
  a.t_internal.join();
  return 0;
}

感谢您的选择。

bind将绑定的参数作为左值传递,并完美地转发传递给其operator()的任何内容。 std::thread将所有内容都作为右值传递。净结果是f的第一个参数需要被复制,而第二个参数则被移动。 - T.C.
@T.C.,谢谢。我现在明白了。我原以为bind和thread有相同的行为。我不知道当调用这些函数时它们是不同的。你能帮我看一下第五个问题吗?我刚刚更新了它。我相信它是有效的,但只需要有人确认一下。谢谢。 - user534498
1个回答

3
(1) 我很好奇为什么在将functor和t2传递给线程之前,需要进行t2移动和t1复制?
这是一个内部实现细节。发生的情况是,`thread`的构造函数将首先使用类似于`std::bind`的内部绑定器绑定functor和提供的参数,然后将生成的绑定functor移动到分配用于存储它的内存中。
(2) 为什么在调用f()之前需要对t2进行MOVE和t1进行COPY?
`std::thread`执行`INVOKE(DECAY_COPY(std::forward(f)), DECAY_COPY(std::forward(args))...)`。 `DECAY_COPY`始终返回一个rvalue,因此`std::thread`将所有内容都作为rvalue传递。
`std::bind`会将绑定的参数作为lvalue传递,并完美地转发传递给其`operator()`的内容。最终的结果是f的第一个参数从lvalue构造(因此是复制),而第二个参数从rvalue构造(因此是移动)。
(3) 为什么t1仍然是copy?这与(2)有关。
`std::bind`将绑定参数作为lvalue传递。
(4) `bind`和` thread`的内部实现不是存储对象`t1`和`t2`吗?这些是lvalue,为什么调用`Test &`会失败呢?我很好奇标准是怎么说的。
第二个参数被作为rvalue传递,这不会绑定到`Test&`。
(5) 我只想确认一下这是否有效,并且不存在悬空引用的危险,即使我们将线程`t`存储在其他地方。例如,以下代码是否仍然有效?
没问题。销毁`std::thread`对象不会销毁线程的参数。它们将一直存在,直到线程终止。毕竟,`detach()`需要正常工作。

非常感谢。现在对我来说非常清楚了。将rvalue参数提供给std::bind和std::thread构造函数,会将参数移动到内部存储中。在调用函数时,std::bind会完美地将内部对象转发给函数,std::thread会将内部对象移动到函数中。 - user534498
另一个问题是,如果您传递std::ref(),那么std::thread不会有点危险吗?因为在调用时它将传递rvalue,原始对象将被销毁,对吗? - user534498
经过一些测试,我认为如果将std::ref传递给线程,那么在调用函数调用时它会完美地转发。否则,它将传递rvalue。希望我没有弄错。谢谢! - user534498
1
@user534498 不,bind总是将内部对象作为lvalues传递。如果您传递std::ref,则承诺在线程终止之前不会销毁引用的对象。然后,在调用线程的函数时,将调用reference_wrapper的转换运算符以获取lvalue引用。 - T.C.

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