为什么编译器抱怨std::thread参数必须在转换为rvalues后可调用?

19
编译器为什么会抱怨如果将线程函数声明更改为void thr(std::shared_ptr<Base>& p)。编译错误:

gcc-10.1.0/include/c++/10.1.0/thread:在' std :: thread :: thread(_Callable&&, _Args&& ...) [with _Callable = void (&)(std::shared_ptr&);_Args = {std::shared_ptr&};= void]'的实例中: gcc-10.1.0/include/c++/10.1.0/thread:136:44: 错误:静态断言失败:std :: thread参数必须在转换为rvalue后可调用

136 | typename decay<_Args>::type...>::value,

请问有人能够逐步解释一下吗?

感谢任何关于这个问题的提示。
#include <iostream>
#include <memory>
#include <thread>
#include <chrono>
#include <mutex>

struct Base
{
    Base() { std::cout << "  Base::Base()\n"; }
    // Note: non-virtual destructor is OK here
    ~Base() { std::cout << "  Base::~Base()\n"; }
};

struct Derived: public Base
{
    Derived() { std::cout << "  Derived::Derived()\n"; }
    ~Derived() { std::cout << "  Derived::~Derived()\n"; }
};

void thr(std::shared_ptr<Base> p)
{
    std::this_thread::sleep_for(std::chrono::seconds(1));
    std::shared_ptr<Base> lp = p; // thread-safe, even though the
                                  // shared use_count is incremented
    {
        static std::mutex io_mutex;
        std::lock_guard<std::mutex> lk(io_mutex);
        std::cout << "local pointer in a thread:\n"
                  << "  lp.get() = " << lp.get()
                  << ", lp.use_count() = " << lp.use_count() << '\n';
    }
}

int main()
{
    std::shared_ptr<Base> p = std::make_shared<Derived>();

    std::cout << "Created a shared Derived (as a pointer to Base)\n"
              << "  p.get() = " << p.get()
              << ", p.use_count() = " << p.use_count() << '\n';
    std::thread t1(thr, p), t2(thr, p), t3(thr, p);
    p.reset(); // release ownership from main
    std::cout << "Shared ownership between 3 threads and released\n"
              << "ownership from main:\n"
              << "  p.get() = " << p.get()
              << ", p.use_count() = " << p.use_count() << '\n';
    t1.join(); t2.join(); t3.join();

    std::cout << "after joining the threads\n" <<
     "  p.get() = " << p.get() << ", p.use_count() " <<p.use_count() << std::endl;
    std::cout << "All threads completed, the last one deleted Derived\n";
}

输出结果:

Base::Base()
  Derived::Derived()
Created a shared Derived (as a pointer to Base)
  p.get() = 0x57be80, p.use_count() = 1
Shared ownership between 3 threads and released
ownership from main:
  p.get() = 0, p.use_count() = 0
local pointer in a thread:
  lp.get() = 0x57be80, lp.use_count() = 4  
local pointer in a thread:
  lp.get() = 0x57be80, lp.use_count() = 3
local pointer in a thread:
  lp.get() = 0x57be80, lp.use_count() = 2
  Derived::~Derived()
  Base::~Base()
after joining the threads
  p.get() = 0, p.use_count() 0
All threads completed, the last one deleted Derived

5
你需要考虑每个线程函数都有两个共享指针实例吗? - Some programmer dude
1
创建+1。调用者将+1、+1、+1(对于每个3个线程实例)作为参数传递给函数参数。主程序执行重置,-1。thr函数进行本地复制,+1、+1、+1。 - Eljay
1
如果您希望线程只有一个指针实例,则可以跳过lp变量,或将p移入lp - Some programmer dude
2
lp.use_count() 的结果取决于线程的调度方式,你当前代码中线程中 lp.use_count() 显示的数字是不可预测的。 - t.niese
2
是的,我注意到了 sleep_for。对于使用计数为5的“How to explain it?”是因为所有3个生成的线程之间存在竞争条件。 - Eljay
显示剩余13条评论
2个回答

35

传递给 std::thread 构造函数的参数将被复制,然后作为右值转发到在新线程中运行的函数。因此,当您像这样创建一个 std::thread

std::thread t1(thr, p)

参数p将被复制,然后作为右值转发。如果函数thr需要左值引用,则无法使用右值调用。

静态断言告诉您无法使用右值shared_ptr<Base>调用thr(shared_ptr<Base>&)。(在添加静态断言之前,您只会从std::thread的内部深处获得可怕的模板实例化错误,现在的想法是它用英语告诉您出了什么问题)。

将引用传递给函数的解决方案是使用std::ref函数创建一个reference_wrapper对象:

std::thread t1(thr, std::ref(p))

这将创建一个 std::reference_wrapper<std::shared_ptr<Base>>,它被复制并作为 rvalue 转发到 thr,然后该 rvalue 可以转换为 shared_ptr<Base>& 以初始化 thr 函数的参数。

这在https://en.cppreference.com/w/cpp/thread/thread/thread#Notes上也有清楚的解释。


参数p将被复制,然后作为右值转发。为什么要将参数作为右值转发?目的是什么? - John
因为参数的新副本刚刚被创建,所以将它们作为lvalue传递会具有误导性和无益处。为什么要获取对在堆上某个地方创建的临时变量的引用呢?而且没有其他线程引用它。 - Jonathan Wakely
为什么要获取对堆上某个临时变量的引用,而其他线程又没有对其引用?抱歉,我还是有些不理解你的意思。让我糊涂的是,即使我将临时对象作为std::thread的参数传递,它仍然会被复制(即通过移动构造函数) ,因为std::thread的参数是按值传递的,而在实现std::thread时使用的参数已经不再是临时对象(即它们是带名称的对象)。 - John
由于std::thread的参数是通过转发引用传递的,所以不是按值传递(即它们确实有名称的对象)。但这是对用户不可见的实现细节。重点是在创建std::thread时,调用者栈上的thread构造函数参数,在新的执行线程中运行提供的函数时可能已经不存在了。为避免悬空引用,std::thread对其所有参数进行右值复制(因此如果要传递引用,您必须明确且小心)。 - Jonathan Wakely
在新的执行线程中调用提供的函数时,会传递临时对象(即rvalue),这些临时对象是通过复制传递给std::thread构造函数的参数而创建的。实现上,副本实际上必须由实现存储在某个地方,然后转发到函数中,这是一个实现细节。可观察的API是将参数复制以创建临时对象,并将这些临时对象传递到新的线程中的函数中。我认为这并不难理解。将它们作为左值转发将没有任何意义。 - Jonathan Wakely
虽然这不适用于这个特定的问题(但它在搜索中排名很高,所以我留下了评论),如果没有参考关系,或者在将所有参考关系添加std::ref之后仍然出现,检查传递的参数数量是一个好主意。在得到有用的错误消息之前,我切换到了std::bind。 我甚至将函数调用转换为所有引用,并使用了std::ref来进行实验。整个函数调用由指针构成,因此std::ref最初不适用于我的情况。我错过了从函数调用中传递的一个参数。 - Zoe stands with Ukraine

2

总结:如何通过std::ref()std::cref()包装器正确地将引用传递给std::thread构造函数

使用std::ref()包装引用参数,使用std::cref()包装const引用参数:

// Function prototypes
void foo(Data& data);           // takes a reference parameter
void foo2(const Data& data);    // takes a const reference parameter

// std::thread constructor calls with proper `std::ref()` and `std::cref()` 
// wrappers
std::thread t1 = std::thread(foo, std::ref(data));
std::thread t2 = std::thread(foo2, std::cref(data));

问题

我似乎无法在 std::thread() 构造函数中使用引用参数。我本来想将这个问题作为一个单独的问答来解决,但是后来发现了这个问题。下面是来自g++的完整错误输出,以便更容易地搜索此问题。

我的示例:

#include <cstdint>  // For `uint8_t`, `int8_t`, etc.
#include <cstdio>   // For `printf()`
#include <iostream>  // For `std::cin`, `std::cout`, `std::endl`, etc.
#include <thread>

struct Data
{
    int i = 7;
};

// Accepts a reference
void foo(Data& data)
{
    printf("data.i = %i\n", data.i);
}

int main()
{
    printf("`std::thread` test\n");

    Data data;
    std::thread t1 = std::thread(foo, data);  // <========= here
    t1.join();

    return 0;
}

示例构建命令和输出:

eRCaGuy_hello_world/cpp$ g++ -Wall -Wextra -Werror -O3 -std=c++17 -pthread std_thread__pass_parameter_by_reference_to_constructor.cpp -o bin/a && bin/a
In file included from std_thread__pass_parameter_by_reference_to_constructor.cpp:87:
/usr/include/c++/8/thread: In instantiation of ‘std::thread::thread(_Callable&&, _Args&& ...) [with _Callable = void (&)(Data&); _Args = {Data&}; <template-parameter-1-3> = void]’:
std_thread__pass_parameter_by_reference_to_constructor.cpp:105:43:   required from here
/usr/include/c++/8/thread:120:17: error: static assertion failed: std::thread arguments must be invocable after conversion to rvalues
  static_assert( __is_invocable<typename decay<_Callable>::type,
                 ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
           typename decay<_Args>::type...>::value,
           ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/usr/include/c++/8/thread: In instantiation of ‘struct std::thread::_Invoker<std::tuple<void (*)(Data&), Data> >’:
/usr/include/c++/8/thread:132:22:   required from ‘std::thread::thread(_Callable&&, _Args&& ...) [with _Callable = void (&)(Data&); _Args = {Data&}; <template-parameter-1-3> = void]’
std_thread__pass_parameter_by_reference_to_constructor.cpp:105:43:   required from here
/usr/include/c++/8/thread:250:2: error: no matching function for call to ‘std::thread::_Invoker<std::tuple<void (*)(Data&), Data> >::_M_invoke(std::thread::_Invoker<std::tuple<void (*)(Data&), Data> >::_Indices)’
  operator()()
  ^~~~~~~~
/usr/include/c++/8/thread:241:4: note: candidate: ‘template<long unsigned int ..._Ind> decltype (std::__invoke((_S_declval<_Ind>)()...)) std::thread::_Invoker<_Tuple>::_M_invoke(std::_Index_tuple<_Ind ...>) [with long unsigned int ..._Ind = {_Ind ...}; _Tuple = std::tuple<void (*)(Data&), Data>]’
    _M_invoke(_Index_tuple<_Ind...>)
    ^~~~~~~~~
/usr/include/c++/8/thread:241:4: note:   template argument deduction/substitution failed:
/usr/include/c++/8/thread: In substitution of ‘template<long unsigned int ..._Ind> decltype (std::__invoke(_S_declval<_Ind>()...)) std::thread::_Invoker<std::tuple<void (*)(Data&), Data> >::_M_invoke<_Ind ...>(std::_Index_tuple<_Ind ...>) [with long unsigned int ..._Ind = {0, 1}]’:
/usr/include/c++/8/thread:250:2:   required from ‘struct std::thread::_Invoker<std::tuple<void (*)(Data&), Data> >’
/usr/include/c++/8/thread:132:22:   required from ‘std::thread::thread(_Callable&&, _Args&& ...) [with _Callable = void (&)(Data&); _Args = {Data&}; <template-parameter-1-3> = void]’
std_thread__pass_parameter_by_reference_to_constructor.cpp:105:43:   required from here
/usr/include/c++/8/thread:243:29: error: no matching function for call to ‘__invoke(std::__tuple_element_t<0, std::tuple<void (*)(Data&), Data> >, std::__tuple_element_t<1, std::tuple<void (*)(Data&), Data> >)’
    -> decltype(std::__invoke(_S_declval<_Ind>()...))
                ~~~~~~~~~~~~~^~~~~~~~~~~~~~~~~~~~~~~
In file included from /usr/include/c++/8/tuple:41,
                 from /usr/include/c++/8/bits/unique_ptr.h:37,
                 from /usr/include/c++/8/memory:80,
                 from /usr/include/c++/8/thread:39,
                 from std_thread__pass_parameter_by_reference_to_constructor.cpp:87:
/usr/include/c++/8/bits/invoke.h:89:5: note: candidate: ‘template<class _Callable, class ... _Args> constexpr typename std::__invoke_result<_Functor, _ArgTypes>::type std::__invoke(_Callable&&, _Args&& ...)’
     __invoke(_Callable&& __fn, _Args&&... __args)
     ^~~~~~~~
/usr/include/c++/8/bits/invoke.h:89:5: note:   template argument deduction/substitution failed:
/usr/include/c++/8/bits/invoke.h: In substitution of ‘template<class _Callable, class ... _Args> constexpr typename std::__invoke_result<_Functor, _ArgTypes>::type std::__invoke(_Callable&&, _Args&& ...) [with _Callable = void (*)(Data&); _Args = {Data}]’:
/usr/include/c++/8/thread:243:29:   required by substitution of ‘template<long unsigned int ..._Ind> decltype (std::__invoke(_S_declval<_Ind>()...)) std::thread::_Invoker<std::tuple<void (*)(Data&), Data> >::_M_invoke<_Ind ...>(std::_Index_tuple<_Ind ...>) [with long unsigned int ..._Ind = {0, 1}]’
/usr/include/c++/8/thread:250:2:   required from ‘struct std::thread::_Invoker<std::tuple<void (*)(Data&), Data> >’
/usr/include/c++/8/thread:132:22:   required from ‘std::thread::thread(_Callable&&, _Args&& ...) [with _Callable = void (&)(Data&); _Args = {Data&}; <template-parameter-1-3> = void]’
std_thread__pass_parameter_by_reference_to_constructor.cpp:105:43:   required from here
/usr/include/c++/8/bits/invoke.h:89:5: error: no type named ‘typein ‘struct std::__invoke_result<void (*)(Data&), Data>’

有很多需要注意的地方,但是我首先注意到的错误是:

/usr/include/c++/8/thread:120:17: error: static assertion failed: std::thread arguments must be invocable after conversion to rvalues

问题出在哪里呢?

解决方法

你不能直接将引用传递给 std::thread() 构造函数。必须使用 std::ref() 对它们进行包装。

查看这里的 std::thread::thread() 构造函数参考页面:https://en.cppreference.com/w/cpp/thread/thread/thread

线程函数的参数通过值移动或复制。如果需要将引用参数传递给线程函数,则必须对其进行包装(例如,使用 std::refstd::cref )。

因此,用 std::ref() 包装引用参数,并用 std::cref() 包装 const 引用参数,例如:

std::thread t1 = std::thread(foo, std::ref(data));
std::thread t2 = std::thread(foo2, std::cref(data));

完整示例:

std_thread__pass_parameter_by_reference_to_constructor_via_std_ref.cpp

// C & C++ includes
#include <cstdint>  // For `uint8_t`, `int8_t`, etc.
#include <cstdio>   // For `printf()`
#include <iostream>  // For `std::cin`, `std::cout`, `std::endl`, etc.
#include <thread>

struct Data
{
    int i = 7;
};

// Accepts a reference
void foo(Data& data)
{
    printf("data.i = %i\n", data.i);
}

// Accepts a const reference
void foo2(const Data& data)
{
    printf("data.i = %i\n", data.i);
}

// int main(int argc, char *argv[])  // alternative prototype
int main()
{
    printf("`std::thread` test\n");

    Data data;
    std::thread t1 = std::thread(foo, std::ref(data));
    std::thread t2 = std::thread(foo2, std::cref(data));
    t1.join();
    t2.join();

    return 0;
}

参考资料

  1. 我最初学习这个的地方:C++11 std::thread accepting function with rvalue parameter;在这个答案下面看到我的评论
  2. https://en.cppreference.com/w/cpp/thread/thread/thread

    线程函数的参数是按值移动或复制的。如果需要将引用参数传递给线程函数,则必须进行包装(例如,使用std::refstd::cref)。


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