I am trying to pass a lambda-closure to std::thread that calls arbitrary closed-over function with arbitrary closed-over arguments.
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 so that r-value references and l-value references get forwarded (dispatched correctly).std::invoke and the lambda create temporary data-structures, the caller must wrap references in std::ref.The code appears to work, but causes stack-use-after-scope with address sanitization. This is my primary confusion.
I think this may be related to this error, but I do not see the relation since I am not returning a reference; The reference to i should be valid for the duration of main's stack-frame which should outlast the thread because main joins on it. The reference is passed by copies (std::reference_wrapper) into the thread_thunk.
I suspect args... cannot be captured by reference, but then how should it be captured?
A secondary confusion: changing {std::thread t = timed_thread(blah); t.join();} (braces to force destructor) to timed_thread(blah).join(); incurs no such problem, even though to me they appear equivalent.
#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;
}
Compiler command: clang++ -pthread -std=c++17 -Wall -Wextra -fsanitize=address test.cpp && ./a.out. Remvoe address to see it work.
ASAN backtrace
Both versions appear to be undefined behavior. It is potluck whether the undefined behavior will be caught by the sanitizer. It is fairly likely that even the allegedly working version will also trip the sanitizer, if the program is rerun sufficient amount of times. The bug is here:
std::thread timed_thread(Function&& f, Args&&... args) {
// Regarding capturing perfectly-forwarded variables in lambda, see [1]
auto thread_thunk = ([&] {
The closure uses the captured args by reference.
As you know, the parameters to timed_thread go out of scope and get destroyed when timed_thread returns. That's their scope. That's how C++ works.
But you have no guarantees, whatsoever, that this closure gets executed by the new execution thread and references the captured, by reference, all the args..., before they vanish in a puff of smoke here:
return std::thread{thread_thunk};
Unless this new thread manages to execute the code inside thread_hunk, that references the captured, by reference args..., it will end up accessing after this function returns, and this results in undefined behavior.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With