{"id":106870,"date":"2022-07-18T07:00:00","date_gmt":"2022-07-18T14:00:00","guid":{"rendered":"https:\/\/devblogs.microsoft.com\/oldnewthing\/?p=106870"},"modified":"2022-07-18T06:39:19","modified_gmt":"2022-07-18T13:39:19","slug":"20220718-00","status":"publish","type":"post","link":"https:\/\/devblogs.microsoft.com\/oldnewthing\/20220718-00\/?p=106870","title":{"rendered":"C++ coroutine gotcha: Falling off the end of a function-level catch"},"content":{"rendered":"<p>Allowing execution to flow off the end of a coroutine is equivalent to performing a <code>co_return<\/code> with no operand, assuming the coroutine completes with no value. Otherwise, the behavior is undefined.<\/p>\n<p>This is the same rule as with regular functions, just with the letters &#8220;co&#8221; in front.<\/p>\n<table class=\"cp3\" style=\"border-collapse: collapse;\" border=\"1\" cellspacing=\"0\" cellpadding=\"3\">\n<tbody>\n<tr>\n<td>\n<pre>void f1()\r\n{\r\n    DoSomething();\r\n    \/\/ implicit \"return\"\r\n}\r\n<\/pre>\n<\/td>\n<td>\n<pre>simple_task&lt;void&gt; f1co()\r\n{\r\n    co_await DoSomething();\r\n    \/\/ implicit \"co_return\"\r\n}\r\n<\/pre>\n<\/td>\n<\/tr>\n<tr>\n<td>\n<pre>int f2()\r\n{\r\n    DoSomething();\r\n    \/\/ illegal fall-of-the-end\r\n}\r\n<\/pre>\n<\/td>\n<td>\n<pre>simple_task&lt;int&gt; f2co()\r\n{\r\n    co_await DoSomething();\r\n    \/\/ illegal fall-of-the-end\r\n}\r\n<\/pre>\n<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>Unfortunately, many compilers (as of this writing) aren&#8217;t consistent in diagnosing this type of programming error.<\/p>\n<pre>simple_task&lt;int&gt; f2co()\r\n{\r\n    co_await DoSomething();\r\n    \/\/ illegal fall-of-the-end\r\n}\r\n<\/pre>\n<pre style=\"white-space: pre-wrap;\">\/\/ gcc 11.3 -std=c++20 -Wall\r\n(no errors or warnings)\r\n\r\n\/\/ clang 14.0.0 -std=c++20 -Wall\r\nwarning: non-void coroutine does not return a value\r\n\r\n\/\/ msvc 19.31 -std:c++20 -W4\r\nwarning C4033: 'f2co' must return a value\r\nnote: Flowing off the end of a coroutine results in undefined behavior when promise type 'std::<wbr \/>coroutine_traits::<wbr \/>promise_type' does not declare 'return_void'\r\n<\/pre>\n<p>In this case, clang and msvc notice that you forgot to return a value, but gcc doesn&#8217;t notice.<\/p>\n<p>If we tweak the function slightly, we get different results:<\/p>\n<pre>simple_task&lt;int&gt; f3co()\r\n{\r\n    if (Maybe()) co_return 1;\r\n    \/\/ illegal fall-of-the-end\r\n}\r\n<\/pre>\n<pre style=\"white-space: pre-wrap;\">\/\/ gcc 11.3 -std=c++20 -Wall\r\n(no errors or warnings)\r\n\r\n\/\/ clang 14.0.0 -std=c++20 -Wall\r\nwarning: non-void coroutine does not return a value in all control paths\r\n\r\n\/\/ msvc 19.31 -std:c++20 -W4\r\n(no errors or warnings)\r\n<\/pre>\n<p>Adding a <code>co_return<\/code> on one branch of an <code>if<\/code> statement is enough to fool msvc; it doesn&#8217;t notice that there&#8217;s still a code path that fails to <code>co_return<\/code> something.<\/p>\n<p>And then there&#8217;s this wrinkle:<\/p>\n<pre>simple_task&lt;int&gt; f4co() try\r\n{\r\n    co_return 1;\r\n}\r\ncatch (...)\r\n{\r\n    \/\/ illegal fall-of-the-end\r\n}\r\n<\/pre>\n<pre style=\"white-space: pre-wrap;\">\/\/ gcc 11.3 -std=c++20 -Wall\r\n(no errors or warnings)\r\n\r\n\/\/ clang 14.0.0 -std=c++20 -Wall\r\nwarning: non-void coroutine does not return a value in all control paths\r\n\r\n\/\/ msvc 19.31 -std:c++20 -W4\r\n(no errors or warnings)\r\n<\/pre>\n<p>The <code>catch<\/code> block fails to <code>co_return<\/code> anything, which makes it an illegal fall-off-the-end, but gcc and msvc fail to detect it.<\/p>\n<p>This particular failure is easy to miss if you use the WIL exception handling macros like <code>CATCH_LOG<\/code>:<\/p>\n<pre>simple_task&lt;int&gt; f4co() try\r\n{\r\n    co_return 1;\r\n}\r\nCATCH_LOG(); \/\/ invisible fall-off-the-end\r\n<\/pre>\n<p>The <code>CATCH_LOG<\/code> macro catches all exceptions, logs them through the WIL infrastructure, and then falls off the end of the function. It is intended to be used only in cases where falling off the end is allowed (namely, function returning <code>void<\/code> or coroutine completing with <code>void<\/code>). If you use it in a coroutine that has a completion value, then you will just fall off the end, and if you&#8217;re unlucky, the error will go undiagnosed, and you&#8217;re off in undefined territory.<\/p>\n<p><b>Bonus chatter<\/b>: But really, what happens when you fall off the end of the coroutine without <code>co_return<\/code>ing a value? As I noted, it&#8217;s technically undefined behavior, but in practice what happens is that the promise&#8217;s <code>return_value<\/code> method is never called before reaching <code>final_suspend<\/code>. What happens next depends on how the promise is implemented.<\/p>\n<p>In our <code>simple_task<\/code>, it means that the promise state remains <code>empty<\/code>, and then when you try to <code>co_await<\/code> the <code>simple_task<\/code>, <a href=\"https:\/\/devblogs.microsoft.com\/oldnewthing\/20210420-28\/?p=105128\"> the <code>get_value<\/code> method hits an assertion failure and then forcibly <code>std::terminate<\/code>s the program<\/a>.<\/p>\n<p>For PPL tasks, the promise implementation just returns a default-constructed result; For hat-types, that means a null pointer. For C++\/WinRT asynchronous operations, the promise implementation returns an empty result: For value types, you get a default-constructed value type; for reference types, you get a null pointer.<\/p>\n<p>This can lead to a good amount of head-scratching when you <code>co_await<\/code> the task or asynchronous operation and get an empty result \/ null pointer even though you go back to the code and see that at no point does it ever <code>co_return nullptr;<\/code>.<\/p>\n<p>From what you can tell, the compiler appears to have lost its mind, but really, you&#8217;re the one who went crazy. You just didn&#8217;t realize it.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>You still have to return something, but today&#8217;s compilers don&#8217;t warn you.<\/p>\n","protected":false},"author":1069,"featured_media":111744,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[1],"tags":[25],"class_list":["post-106870","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-oldnewthing","tag-code"],"acf":[],"blog_post_summary":"<p>You still have to return something, but today&#8217;s compilers don&#8217;t warn you.<\/p>\n","_links":{"self":[{"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/posts\/106870","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/users\/1069"}],"replies":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/comments?post=106870"}],"version-history":[{"count":0,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/posts\/106870\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/media\/111744"}],"wp:attachment":[{"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/media?parent=106870"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/categories?post=106870"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/tags?post=106870"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}