正如清单 2.4 所示,向可调用对象或函数传递参数很简单,只需要将这些参数作为 std::thread 构造函数的附加参数即可。但需要注意的是,在默认情况下,这些参数会被拷贝至新线程的独立内存空间中,以供新线程访问,并如同临时变量一样作为右值传递给可调用对象或函数。即使函数中的参数是引用的形式,拷贝操作也会执行。再来看一个例子:

    1. void f(int i, std::string const& s);
    2. std::thread t(f, 3, "hello");

    代码创建了一个调用f(3, “hello”)的线程。注意,函数f需要一个std::string对象作为第二个参数,但这里使用的是字符串的字面值,也就是char const *类型。之后,在线程的上下文中完成字面值向std::string对象的转化。特别需要注意的是,当指向动态变量的指针作为参数传递给线程的情况,代码如下:

    void f(int i,std::string const& s);
    void oops(int some_param)
    {
      char buffer[1024]; // 1
      sprintf(buffer, "%i",some_param);
      std::thread t(f,3,buffer); // 2
      t.detach();
    }
    

    这种情况下,buffer① 是一个指针变量,指向局部变量,然后此局部变量通过 buffer 传递到新线程中②。此时,函数 oops 很有可能会在 buffer 转换成std::string对象之前结束,从而导致一些未定义的行为。因为此时无法保证隐式转换的操作和 std::thread 构造函数的拷贝操作按顺序进行,有可能 std::thread 的构造函数拷贝的是转换前的变量(buffer 指针),而非字符串。解决方案就是在传递到std::thread构造函数之前就将字面值转化为std::string对象:

    void f(int i,std::string const& s);
    void not_oops(int some_param)
    {
      char buffer[1024];
      sprintf(buffer,"%i",some_param);
      std::thread t(f,3,std::string(buffer));  // 使用std::string,避免悬垂指针
      t.detach();
    }
    

    相反的情形(期望传递一个非常量引用,但整个对象被复制)倒是不会出现,因为其不能编译。比如,你可能会尝试使用线程更新一个引用传递的数据结构:

    void update_data_for_widget(widget_id w,widget_data& data); // 1
    void oops_again(widget_id w)
    {
      widget_data data;
      std::thread t(update_data_for_widget,w,data); // 2
      display_status();
      t.join();
      process_widget_data(data);
    }
    

    虽然 update_data_for_widget① 的第二个参数期待传入一个引用,但是 std::thread 的构造函数②并不知晓;构造函数无视函数期待的参数类型,并盲目地拷贝已提供的变量。不过,内部代码会将拷贝的参数以右值的方式进行传递,这是为了照顾到那些只能进行移动的类型,而后会尝试以右值为实参调用 update_data_for_widget。但因为函数期望的是一个非常量引用作为参数,而非右值,所以会在编译时出错。对于熟悉std::bind的开发者来说,问题的解决办法是显而易见的:可以使用std::ref将参数转换成引用的形式,因此可将线程的调用改为以下形式:

    std::thread t(update_data_for_widget,w,std::ref(data));
    

    从而,update_data_for_widget 就会接收到一个 data 变量的引用,而非 data 变量的拷贝副本,这样代码就能顺利的通过编译。

    如果你熟悉 std::bind,就应该不会对以上述传参的语法感到陌生,因为 std::thread 构造函数和std::bind 的操作在标准库中以相同的机制进行定义。比如,你也可以传递一个成员函数指针作为线程函数,并提供一个合适的对象指针作为第一个参数:

    class X
    {
    public:
      void do_lengthy_work();
    };
    X my_x;
    std::thread t(&X::do_lengthy_work,&my_x); // 1
    

    这段代码中,新线程将会调用 my_x.do_lengthy_work(),其中 my_x 的地址 ① 作为对象指针提供给函数。你也可以为成员函数提供参数:std::thread 构造函数的第三个参数就是成员函数的第一个参数,以此类推(代码如下,译者自加)。

    class X
    {
    public:
      void do_lengthy_work(int);
    };
    X my_x;
    int num(0);
    std::thread t(&X::do_lengthy_work, &my_x, num);
    

    另一种有趣的情形是,提供的参数仅支持移动(move),不能执行拷贝。”移动” 是指: 原始对象中的数据所有权转移给另一对象,从而转移的这些数据就不再在原始对象中保存(译者:比较像在文本编辑的”剪切”操作)。std::unique_ptr就是这样一种类型(译者:C++11中的智能指针),这种类型为动态分配的对象提供内存自动管理机制(译者:类似垃圾回收)。同一时间内,只允许一个 std::unique_ptr 实例指向一个给定对象,并且当这个实例销毁时,指向的对象也将被删除。移动构造函数(move constructor)和移动赋值操作符(move assignment operator)允许一个对象的所有权在多个 std::unique_ptr 实例中传递(有关”移动”的更多内容,请参考附录A的A.1.1节)。使用”移动”转移原对象后,就会留下一个空指针(NULL)。使用移动操作可以将对象转换成函数可接受的实参类型,或满足函数返回值类型要求。当原对象是一个临时变量时,自动进行移动操作,但当原对象是一个命名变量,那么转移的时候就需要使用std::move()进行显示移动。下面的代码展示了std::move的用法,展示了std::move是如何转移一个动态对象的所有权到一个线程中去的:

    void process_big_object(std::unique_ptr<big_object>);
    
    std::unique_ptr<big_object> p(new big_object);
    p->prepare_data(42);
    std::thread t(process_big_object,std::move(p));
    

    通过在 std::thread 的构造函数中执行 std::move(p),big_object 对象的所有权首先被转移到新创建线程的的内部存储中,之后再被传递给 process_big_object 函数。

    C++ 标准线程库中和std::unique_ptr在所属权上有相似语义类型的类有好几种,std::thread为其中之一。虽然,std::thread实例不像std::unique_ptr那样能占有一个动态对象的所有权,但是它能占有其他资源:每个实例都负责管理一个执行线程。执行线程的所有权可以在多个std::thread实例中互相转移,这是依赖于std::thread实例的可移动不可复制性。不可复制保性证了在同一时间点,一个std::thread实例只能关联一个执行线程;可移动性使得开发者可以自己决定,哪个实例拥有实际执行线程的所有权。