我正试图将lambda-closure传递到 std::thread
调用任意闭合参数的闭合函数。
template< class Function, class... Args >
std::thread timed_thread(Function&& f, Args&&... args) {
// Regarding capturing perfectly-forwarded variables in lambda, see [1]
auto thread_thunk = ([&] {
std::cout << "Start thread timer" << std::endl;
// Regarding std::invoke(_decay_copy(...), ...), see (3) of [2].
// Assume no exception can be thrown from copying.
std::invoke(_decay_copy(std::forward<Function>(f)),
_decay_copy(std::forward<Args>(args)...));
}
}
int main() {
int i = 3;
std::thread t = timed_thread(&print_int_ref, std::ref(i));
t.join()
return 0;
}
/*
[1]: https://stackoverflow.com/questions/26831382/capturing-perfectly-forwarded-variable-in-lambda
[2]: https://en.cppreference.com/w/cpp/thread/thread/thread
*/
std::forward
这样,r值引用和l值引用就会得到 转发 (正确派遣)。std::invoke
和lambda创建临时数据结构,调用者必须将引用包裹在 std::ref
.该代码似乎可以工作,但会导致 stack-use-after-scope
与地址消毒。这是我的主要困惑。
我想这可能与 这个错误但我看不到关系,因为我没有返回一个引用;对 i
的有效期内。main
的堆栈框架,它应该比线程的时间长,因为 main
上加入。引用是通过拷贝(std::reference_wrapper
)进入 thread_thunk
.
我怀疑 args...
不能参照捕捉,但又该如何捕捉呢?
第二种困惑:改变 {std::thread t = timed_thread(blah); t.join();}
(大括号强制拆解器)至 timed_thread(blah).join();
不存在这样的问题,尽管在我看来它们是等同的。
#include <functional>
#include <iostream>
#include <thread>
template <class T>
std::decay_t<T> _decay_copy(T&& v) { return std::forward<T>(v); }
template< class Function, class... Args >
std::thread timed_thread(Function&& f, Args&&... args) {
// Regarding capturing perfectly-forwarded variables in lambda, see [1]
auto thread_thunk = ([&] {
std::cout << "Start thread timer" << std::endl;
// Regarding std::invoke(_decay_copy(...), ...), see (3) of [2].
// Assume no exception can be thrown from copying.
std::invoke(_decay_copy(std::forward<Function>(f)),
_decay_copy(std::forward<Args>(args)...));
std::cout << "End thread timer" << std::endl;
});
/* The single-threaded version code works perfectly */
// thread_thunk();
// return std::thread{[]{}};
/* multithreaded version appears to work
but triggers "stack-use-after-scope" with ASAN */
return std::thread{thread_thunk};
}
void print_int_ref(int& i) { std::cout << i << std::endl; }
int main() {
int i = 3;
/* This code appears to work
but triggers "stack-use-after-scope" with ASAN */
// {
// std::thread t = timed_thread(&print_int_ref, std::ref(i));
// t.join();
// }
/* This code works perfectly */
timed_thread(&print_int_ref, std::ref(i)).join();
return 0;
}
编译器命令。clang++ -pthread -std=c++17 -Wall -Wextra -fsanitize=address test.cpp && ./a.out
. Remvoe address
来看看它的工作。
这两个版本似乎都是未定义的行为。未定义的行为是否会被sanitizer捕捉到是大锅饭。如果程序被重新运行的次数足够多的话,即使是所谓的工作版本也很有可能会触发 sanitizer。这个bug就在这里。
std::thread timed_thread(Function&& f, Args&&... args) {
// Regarding capturing perfectly-forwarded variables in lambda, see [1]
auto thread_thunk = ([&] {
封闭使用了捕获到的... args
参照.
如你所知,参数为 timed_thread
亡羊补牢 timed_thread
返回。这就是他们的范围。这就是C++的工作原理。
但是你没有保证。任何该闭包被新的执行线程执行并引用捕获的内容。参照,所有 args...
趁他们还没在这里消失之前。
return std::thread{thread_thunk};
除非这个新的线程能执行里面的代码... ... thread_hunk
的,引用捕获的。参照 args...
,它将在这个函数返回后结束访问,这将导致未定义的行为。
在其生命期结束后,被使用的对象是std::ref(i)。 按照引用的方式。 函数通过引用获取std::ref,lambda通过引用捕获,lambda被复制到新创建的线程中,该线程将引用复制到std::ref(i)。
工作版本之所以有效,是因为std::ref(i)的寿命在分号处结束,而线程在这之前就已经加入了。