MSVC 上关于 std::thread 构造函数的参数传递

在使用 Visual Studio 上写跨平台 c++,经常会遇到一些问题,在 windows 下没有问题, 换个平台编都编不过,这次遇到的问题是有关线程库构造函数的参数传递

问题

我们来看一个简单的程序

#include <cstdio>
#include <thread>

struct A
{
    int i;
};

void add_one(A & r)
{
    ++r.i;
    printf("now i is %d\n", r.i);
}

int main()
{
    A a{ 9 };
    printf("i is %d\n", a.i);

    std::thread t(add_one, a);
    t.join();

    printf("i after thread: %d", a.i);
    return 0;
}

这个程序在 msvc 下编译运行的结果如下

i is 9
now i is 10
i after thread: 9

分析

很显然, msvc 复制了一份 a ,然后把它传递给新创建的线程,所以线程结束后, 主线程中的 a 完全没有变化。但是这份代码在 gcc 上是编译不过的,因为 add_one 接受的是引用,而我们传入了一个变量, 于是在最终 invoke 的步骤时没有找到 invoke 正确的实现,不过不同的 g++ 编译器, 报的错误也不近相同:

  • 在 OSX 下,报错
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include/c++/v1/thread:342:5: error: 
      attempt to use a deleted function
    __invoke(_VSTD::move(_VSTD::get<1>(__t)), _VSTD::move(_VSTD::get<_In...
    ^

这应该是匹配到了 invoke 的空实现

__nat __invoke(__any, ...);
  • 在 centos 7 gcc 4.8.5 下,报错
/usr/include/c++/4.8.2/functional:1697:61: error: no type named ‘type’ in ‘class std::result_of<void (*(int))(int&)>’
       typedef typename result_of<_Callable(_Args...)>::type result_type;
                                                             ^
/usr/include/c++/4.8.2/functional:1727:9: error: no type named ‘type’ in ‘class std::result_of<void (*(int))(int&)>’
         _M_invoke(_Index_tuple<_Indices...>)

result_type 中没有 type 类型,所以 gcc 也没有处理这种类型的参数

解决这个编译错误有两种方法:

  1. add_one 参数改为变量,这样输出结果和 msvc 相同
  2. 创建线程改为 std::thread t(add_one, std::ref(a)); 指明使用引用 ,这样输出结果如下
i is 9
now i is 10
i after thread: 10

好玩的事

其实 msvc 针对引用参数也是做了一定防范的,如果我们把这段代码修改一下,去掉结构体, 直接使用基本类型 int 的话,代码会变成这样

#include <cstdio>
#include <thread>

void add_one(int & r)
{
    ++r;
    printf("now i is %d\n", r);
}

int main()
{
    int i = 9;
    printf("i is %d\n", i);

    std::thread t(add_one, i);
    t.join();

    printf("i after thread: %d\n", i);
    return 0;
}

这时编译, msvc 就会报错了

1>c:\program files (x86)\microsoft visual studio 14.0\vc\include\thr\xthread(240): error C2672: “std::invoke”: 未找到匹配的重载函数

所以 msvc 没有严格遵守或实现这个规则,原因就不得而知了,不过我有点好奇如果我传递引用的话, 结果会是怎样,于是让我们把程序改成

#include <cstdio>
#include <thread>

struct A
{
    int i;
};

void add_one(A & r)
{
    ++(r.i);
    printf("now i is %d\n", r.i);
}

int main()
{
    A a{ 9 };
    A & r = a;
    printf("i is %d\n", a.i);

    std::thread t(add_one, r);
    t.join();

    printf("i after thread: %d\n", a.i);
    return 0;
}

主线程的 i 在完成时依然是 9,不知道你猜对了没有,顺带一提这段代码在 g++ 上依然编不过, 但是当我们改成传递指针的话

#include <cstdio>
#include <thread>

struct A
{
    int i;
};

void add_one(A * r)
{
    ++(r->i);
    printf("now i is %d\n", r->i);
}

int main()
{
    A a{ 9 };
    A * r = &a;
    printf("i is %d\n", a.i);

    std::thread t(add_one, r);
    t.join();

    printf("i after thread: %d\n", a.i);
    return 0;
}

i 最终会变成 10,另外 g++ 也可以正常编译并得到相同的结果了

结论

对于 运行函数传递引用参数 的线程来说,传递变量时要加 std::ref , 不然在 msvc 下虽然编译没有问题,但是会造成变量值的变化不合预期