Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
hide_name to intentionally hide a lambda capture or parameter (#198)
One of the sharp edges of C++ coroutines is lifetime management. A lambda coroutine with captures cannot safely access the captures after the coroutine suspends, because the lambda may have been destructed when the coroutine resumes. A coroutine function cannot safely access any reference parameters after the coroutine suspends, because the caller may have destroyed the referenced objects in the meantime. To avoid around the problem, you have to copy the unsafe things into your frame before your first suspension. ```cpp void Widget::RegisterHandlers(int id) { m_button.Click([id, weak_this = get_weak()](auto&& sender, auto&& args) { // Copy things into the frame for safekeeping before // our first co_await. if (auto strongThis = weak_this.get()) { auto idCopied = id; // After this co_await, "id" and "weak_this" are no longer // safe to use. co_await winrt::resume_background(); // Use the captured variables instead. strongThis->Update(idCopied); } }); } winrt::IAsyncAction Widget::DoSomethingAsync(Thing const& thingRef) { // Copy things into the frame for safekeeping before our first co_await. auto strongThis = get_strong(); auto thing = thingRef; // After this co_await, "thingRef" is no longer safe to use. co_await winrt::resume_background(); // Use the captured variables instead. strongThing->Method(thing); } ``` To prevent accidental use of these unsafe variables, you can intentionally hide them. ```cpp void Widget::RegisterHandlers(int id) { m_button.Click([id, weak_this = get_weak()](auto&& sender, auto&& args) { // Copy things into the frame for safekeeping before // our first co_await. if (auto strongThis = weak_this.get()) { auto idCopied = id; // Hide these names so you can't use them by mistake. // They are about to become unsafe. wil::hide_name id, weak_this; // <<<<<<<<<< NEW // After this co_await, "id" and "weak_this" are no longer // safe to use. co_await winrt::resume_background(); // Use the captured variables instead. strongThis->Update(idCopied); } }); } winrt::IAsyncAction Widget::DoSomethingAsync(Thing const& thingRef) { // Copy things into the frame for safekeeping before our first co_await. auto strongThis = get_strong(); auto thing = thingRef; // Hide this name so you can't use it by mistake. // it is about to become unsafe. wil::hide_name thingRef; // <<<<<<<<<<<<<<<<<< NEW // After this co_await, "thingRef" is no longer safe to use. co_await winrt::resume_background(); // Use the captured variables instead. strongThing->Method(thing); } ``` C++ permits you to hide names from outer scopes, so a function body can hide parameters, member variables, functions and methods, and types defined at outer scopes. A lambda function body can also hide captured variables. Implementation details ====================== We define `hide_name` as a function alias, so when you hide a name, what you're doing is declaring a function with the same name. Using a function has these benefits: * Unlike local variables, you don't get "declared but not used" warnings for functions on any major compiler (msvc, gcc, clang, icc). * If you manage to take the address of the function, thinking that you're taking the address of a captured variable or parameter, you will get an error because function pointers are not implicitly convertible to `void*`. * You can't assign to a function, so attempted assignments are blocked. * If you try to use the name, it will decay to a function pointer that is unusable because it requires you to pass a parameter which is an incomplete type. * If you manage to bypass all these compile-time checks, you get a linker error saying that the function `void name(wil::hidden_name)` does not exist. This is either a clever hack or an abuse of the language. If you try to read the value of a hidden name: * MSVC: error C2664: cannot convert from `wil::hide_name (__cdecl *)` to `T` * MSVC: error C2296: binary operation illegal, left operand has type `wil::hide_name (__cdecl *)` * gcc: error: invalid conversion from `void (*)(wil::hidden_name)` to `T` [-fpermissive] * gcc: warning: pointer to a function used in arithmetic (followed by another error when the result is used) * gcc: error: invalid operands of types `wil::hide_name` {aka `void(wil::hidden_name)`} and `T` to binary operator * clang: error: cannot initialize a variable of type `T` with an lvalue of type `wil::hide_name` (aka `void (struct hidden_name)`) * clang: error: arithmetic on a pointer to the function type `wil::hide_name` (aka `void (struct hidden_name)`) * clang: error: invalid operands to binary expression (`wil::hide_name` (aka `void (struct hidden_name)`) and `T`) * icc: error: value of type "`wil::hide_name *`" cannot be used to initialize an entity of type "`T`" * icc: error: argument of type "`wil::hide_name *`" is incompatible with parameter of type "`T`" * icc: error: expression must have integral or unscoped enum type Assigning to a hidden variable: * msvc: error C2659: '=': function as left operand * gcc: error: assignment of function 'void v(wil::hidden_name)' * clang: error: non-object type `wil::hide_name` (aka `void (struct hidden_name)`) is not assignable * icc: error: expression `name` must be a modifiable lvalue Co-authored-by: Jon Wiswall <[email protected]> Co-authored-by: Duncan Horn <[email protected]>
- Loading branch information