A customer passed a string to a newly-created std::
but found that the std::
received by the thread was already invalid. How can that happen?
Here was how they passed the parameter to the thread:
void OnWidgetChanged(const char* reason) { std::thread backgroundThread(ProcessWidgetChange, reason); backgroundThread.detach(); } void ProcessWidgetChange(std::string reason) { // the reason is corrupted! }
The problem is that the raw const char*
pointer is being converted to a std::
too late.
To avoid confusion, let me rename the two reason
parameters:
void OnWidgetChanged(const char* rawReason) { std::thread backgroundThread(ProcessWidgetChange, rawReason); backgroundThread.detach(); } void ProcessWidgetChange(std::string stringReason) { // the stringReason is corrupted! }
The parameters to the std::
constructor are captured by value, and the copies are then passed to the new thread, which executes the desired code by passing those copies to std::invoke
. The specification calls out that the capturing happens in the calling thread as part of the constructor.
Here’s a flowchart of the operations that take place:
Original thread | |
Capture parameters by value | |
Start new thread | |
⤳ | New thread |
Convert parameters to match signature of target | |
Call the target | |
Destruct converted parameters |
In the above example, it means that the raw const char*
is captured and given to the thread. Only when the thread begins execution is the raw const char*
converted to a std::
, and by that point, it’s too late.
Original thread | |
Copy the rawReason raw pointer |
|
Start new thread | |
⤳ | New thread |
Convert: Construct stringReason from rawReason |
|
Call the target with stringReason |
|
Destruct: Destruct stringReason and rawReason |
The problem is clearer when the conversion is spelled out: The original code has already detached the thread and returned, at which point the caller of OnWidgetChanged
is not under any obligation to keep the pointer valid any further. The conversion to a std::string
therefore happens too late.
The author of the above code was under the incorrect belief that the operations occurred in a different order:
Original thread | |
Convert: Construct stringReason from rawReason |
|
Capture converted parameters | |
Start new thread | |
⤳ | New thread |
Call the target | |
Destruct: Destruct stringReason |
If you think about it, it’s not possible to do things the second way. The thread
constructor could try to infer the signature of the callable, but the callable might have multiple operator()
overloads, so the tricks for deconstructing the function pointer don’t work. And even if they did work, it’s possible that the caller didn’t pass the same number of parameters as the function expects, but expecting it to work because the missing parameters have default values. But there’s no way to deduce the default values (because default values are not part of the function type signature), so the converter doesn’t know what values to use for the missing parameters.
The solution is to convert the rawReason
raw pointer to a std::
while the rawReason
is still valid, pass that converted value to the std::thread
constructor.
void OnWidgetChanged(const char* rawReason)
{
std::thread backgroundThread(ProcessWidgetChange,
std::string(rawReason));
backgroundThread.detach();
}
void ProcessWidgetChange(std::string stringReason)
{
// use the stringReason
}
In the original customer version, the issue wasn’t about an anticipated conversion from a raw pointer to a std::
, but rather an anticipated conversion of a raw COM pointer to a smart one:
void OnWidgetChanged(IWidget* widget) { // Oops, captures raw pointer, not ComPtr. std::thread backgroundThread(ProcessWidgetChange, widget); backgroundThread.detach(); } void ProcessWidgetChange(ComPtr<IWidget> widget) { widget->Refresh(); }
The solution, as before, is to force the conversion early:
void OnWidgetChanged(IWidget* widget)
{
std::thread backgroundThread(&ProcessWidgetChange,
ComPtr<IWidget>(widget));
backgroundThread.detach();
}
Bonus chatter: It would have been nice to use Class Template Argument Deduction (CTAD) to simplify this to
void OnWidgetChanged(IWidget* widget)
{
std::thread backgroundThread(&ProcessWidgetChange,
ComPtr(widget));
backgroundThread.detach();
}
Unfortunately, as we saw earlier, WRL’s ComPtr
doesn’t support CTAD.
Is there a way to use templates to provide a wrapper function that solves this problem for the general case? Something like this:
<code>
If is a plain function pointer, you can pattern-match on its signature:
<code>
But you'd then also want to handle things like member function pointers, const member function pointers, objects with an unoverloaded (very hard), etc. And it won't work at all for objects with overloaded or templated (s).
Edit: Removed "(very hard)" as it can be implemented in terms of the member function pointer overload.
PS: The implementation above never works with functions taking...
That's fascinating! I'm not a C++ programmer myself but I've read enough of this blog to have an appreciation of how powerful and how intricate it can get. The way you're pattern matching on the function's signature is super clever and I wish I'd thought of it. I had the vague idea that it might be possible to template the Params types and Args types separately, but after poring over C++ specs for things like...
Why is it necessary to use a closure here
No, you don't need to use a lambda if you can convert the to without using a tuple. Unfortunately C++ doesn't support syntax like
<code>
but it seems like you can write
<code>
I used a tuple because I totally forgot you can zip-unpack two different parameter packs (of the same length) directly, and I used a lambda because then I needed to use ...
Another annoying wrinkle is that the parameter packs may not be the same length thanks to default parameters.
I also encountered this problem. I have documented it at https://www.codeproject.com/Articles/5377342/VCplusplus-30-Multithreading-Mistakes-on-Windows
Note: for the original problem of COM ptr. COM ptr needs to be marshalled to another thread. It did not prompt any error because the worker thread did not call CoInitialize() to initialize the COM apartment.
EDIT2: In the COM case, it can be solved without using COM smart pointer. Just call AddRef() on the raw interface pointer in the main thread to increment reference count...
Arguably, the root issue is passing `ProcessWidgetChange` as a function pointer. A case can be made that in C++ the name of a function should only ever be treated as an overload set and the only valid operation on an overload set is to call it.
If the code had been written like the following, it would be a bit more clear what the problem is.
<code>
And the solution then becomes:
<code>
---
Of particular note, there are IIRC a...