A customer who used the Windows Implementation Library couldn’t find a CONTINUE_IF_FAILED
macro, the counterpart to the existing RETURN_IF_FAILED
macro, so they wrote their own:
#define CONTINUE_IF_FAILED(hr) \ if (FAILED(hr)) { LOG_HR(hr); continue; }
This version has a serious problem: It evaluates its argument twice.
Their sample usage went something like this:
for (auto&& item : items) { wil::com_ptr<IAttachment> attachment; CONTINUE_IF_FAILED(item->GetAttachment(&attachment)); wil::unique_cotaskmem_ptr name; CONTINUE_IF_FAILED(attachment->GetName(&name)); if (wcscmp(name.get(), L"Special") == 0) { HandleSpecial(item); } }
After expanding the macro one level, we have
for (auto&& item : items) { wil::com_ptr<IAttachment> attachment; if (FAILED(item->GetAttachment(&attachment))) { LOG_HR(item->GetAttachment(&attachment)); continue; } wil::unique_cotaskmem_ptr name; if (FAILED(attachment->GetName(&name))) { LOG_HR(attachment->GetName(&name)); continue; } if (wcscmp(name.get(), L"Special") == 0) { HandleSpecial(item); } }
Notice that in both cases, we attempt the operation, and if it fails, we attempt it again (!) and log the result of the second attempt.
Fortunately in this case, the Get
methods have no side effects, so calling them twice is mostly harmless. You can imagine cases where this could be a problem, say, if the method was something like ToggleÂState
.
Furthermore, the second call might not fail for the same reason as the first call. Maybe the first call failed due to a transient error (server not responding, low memory), and the second call succeeds. Your code logs a success code, and you’ll be scratching your head when you study your logs trying to figure out why the code gave up on a successful call.
You should build your solution out of the existing RETURN_
macro. The wil wiki even explains how: Use a lambda to wrap the block you want to early-exit from without exiting the entire function.
for (auto&& item : items) { [&] { wil::com_ptr<IAttachment> attachment; RETURN_IF_FAILED(item->GetAttachment(&attachment)); wil::unique_cotaskmem_ptr name; RETURN_IF_FAILED(attachment->GetName(&name)); if (wcscmp(name.get(), L"Special") == 0) { HandleSpecial(item); } return S_OK; }(); }
You have all of the RETURN_
macros at your disposal inside the lambda. You can use RETURN_
, RETURN_
, RETURN_
, and so on, so no need to create CONTINUE_
variants of everything. And all of the error logging and reporting settings related to RETURN_
will still work.
This old new article seems very relevant on the topic. Even the comments “fixing” the macros managed to introduce bugs in the fixes
You’re right, yet the correct inference is that I don’t use WIL. The link is a fragment to the wiki, so it can be expected that only the excerpt governed by the linked heading is read.
A more complete rewrite would be
The failure inside the lambda will have already been logged by the
RETURN_IF_FAILED
macro. Did you not read the linked wil wiki page?This definition of CONTINUE_IF_FAILED() has another bug. If you write this:
<code>
it looks like the `else` pairs with with the `if (foo)` predicate. But what actually happens after macro expansion is this:
<code>
This is now a syntax error, because the `else` is all on its own without a matching `if` before it.
The usual solution to this problem is to wrap the macro definition in `do { } while(false)` block, which requires a semicolon to terminate it, so that the semicolon after the invocation gets used for that.
The solution for that is to use
if(!condition) {} else …
The usual do…while(false) solution won’t work here, because the fake loop would change the meaning of “continue”.
Whoops, you are correct.