<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:media="http://search.yahoo.com/mrss/">
  <channel>
    <title>assertions on stillcooking.dev</title>
    <link>https://stillcooking.dev/en/tags/assertions/</link>
    <description>Personal portfolio and technical notes</description>
    <generator>Hugo</generator>
    <language>en-US</language>
    
    <copyright>© 2026 stillcooking.dev — CC BY 4.0, https://creativecommons.org/licenses/by/4.0/</copyright>
    <lastBuildDate>Thu, 29 Oct 2026 00:00:00 +0000</lastBuildDate>
    <atom:link href="https://stillcooking.dev/en/tags/assertions/index.xml" rel="self" type="application/rss+xml" />
    
    <item>
      <title>Catch callback re-entry with checkf and TGuardValue</title>
      <link>https://stillcooking.dev/en/topics/unreal-engine/cpp-in-unreal/reentrancy-guard/</link>
      <pubDate>Thu, 29 Oct 2026 00:00:00 +0000</pubDate>
      
      <guid isPermaLink="true">https://stillcooking.dev/en/topics/unreal-engine/cpp-in-unreal/reentrancy-guard/</guid>
      <description>If a function calls out to a callback in the middle of an update, that callback can call the function again. Two lines, checkf and TGuardValue, turn that quiet bug into an immediate stop with a message that names the cause.</description>
      <content:encoded><![CDATA[<h2 id="the-problem">The problem</h2>
<p>If a function hands control to code it doesn&rsquo;t own in the middle of an update (a delegate, a multicast <code>Broadcast</code>, a visitor, a <code>BlueprintImplementableEvent</code>), that code can call the function again. Usually nothing crashes and nothing reports it. The nested call sees state the outer call is halfway through changing, and guarantees like event order quietly break. You find out later, when the game behaves slightly wrong.</p>
<h2 id="the-fix">The fix</h2>
<p>The fix is two lines at the top of the function. Here they are in the drain loop of my Chronos scheduler (the guard lines are verbatim; the loop is simplified):</p>
<div class="highlight" data-lang="cpp">
  <button type="button" class="code-copy" data-code-copy data-label="Copy" data-copied="Copied!" aria-label="Copy code to clipboard">Copy</button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-cpp" data-lang="cpp"><span class="line"><span class="cl"><span class="n">int32</span> <span class="n">FChronosSchedule</span><span class="o">::</span><span class="n">Drain</span><span class="p">(</span><span class="k">const</span> <span class="n">FDateTime</span><span class="o">&amp;</span> <span class="n">Now</span><span class="p">,</span> <span class="n">TFunctionRef</span><span class="o">&lt;</span><span class="kt">void</span><span class="p">(</span><span class="k">const</span> <span class="n">FChronosFiredEvent</span><span class="o">&amp;</span><span class="p">)</span><span class="o">&gt;</span> <span class="n">Visitor</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">checkf</span><span class="p">(</span><span class="o">!</span><span class="n">bIsDraining</span><span class="p">,</span> <span class="n">TEXT</span><span class="p">(</span><span class="s">&#34;FChronosSchedule::Drain must not be called from its own visitor.&#34;</span><span class="p">));</span>
</span></span><span class="line"><span class="cl">    <span class="n">TGuardValue</span><span class="o">&lt;</span><span class="kt">bool</span><span class="o">&gt;</span> <span class="n">DrainingGuard</span><span class="p">(</span><span class="n">bIsDraining</span><span class="p">,</span> <span class="nb">true</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">int32</span> <span class="n">FiredCount</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="k">while</span> <span class="p">(</span><span class="n">HasDueEvent</span><span class="p">(</span><span class="n">Now</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">    <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="k">const</span> <span class="n">FChronosFiredEvent</span> <span class="n">Fired</span> <span class="o">=</span> <span class="n">PopDueEvent</span><span class="p">(</span><span class="n">Now</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">        <span class="o">++</span><span class="n">FiredCount</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="n">Visitor</span><span class="p">(</span><span class="n">Fired</span><span class="p">);</span>   <span class="c1">// code you don&#39;t control runs here
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">FiredCount</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div>
<p><code>bIsDraining</code> is a plain <code>bool</code> member, initialized to <code>false</code>. If a visitor calls <code>Drain</code> again, the nested call would fire the next due events before the visitor has finished with the current one. Instead, the nested <code>checkf</code> sees <code>true</code> and stops right there, with the cause in the log rather than a symptom three systems away.</p>
<h2 id="how-it-works">How it works</h2>
<p><code>TGuardValue</code> (<code>Templates/UnrealTemplate.h</code>) is a small RAII helper. The constructor saves the current value and assigns the new one. The destructor restores the saved value when execution leaves the scope, including through an early <code>return</code>. Resetting the flag by hand works until someone adds an early return and the flag stays <code>true</code> for good. <code>TGuardValue</code> also restores the original value, not <code>false</code>, which matters once guards nest or the type isn&rsquo;t <code>bool</code>.</p>
<p><code>checkf</code> (<code>Misc/AssertionMacros.h</code>) is a fatal assert. On failure it records the expression, file, line and your message. With a debugger attached it breaks on that line (and you can step past it); without one it raises a fatal error and the process exits, with the callstack in the crash report. It only exists when <code>DO_CHECK</code> is on. <code>Misc/Build.h</code> turns that on in Debug and Development (including the editor); Test and Shipping follow <code>USE_CHECKS_IN_SHIPPING</code>, which defaults to 0. When checks are compiled out, the expression still has to compile but never runs.</p>
<h2 id="choosing-how-to-handle-re-entry">Choosing how to handle re-entry</h2>
<p>The flag and guard stay the same; only how you handle re-entry changes:</p>
<div class="highlight" data-lang="cpp">
  <button type="button" class="code-copy" data-code-copy data-label="Copy" data-copied="Copied!" aria-label="Copy code to clipboard">Copy</button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-cpp" data-lang="cpp"><span class="line"><span class="cl"><span class="c1">// A. Programming error: fatal in Debug/Development, compiled out in Test/Shipping by default.
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="n">checkf</span><span class="p">(</span><span class="o">!</span><span class="n">bIsBusy</span><span class="p">,</span> <span class="n">TEXT</span><span class="p">(</span><span class="s">&#34;Foo() called from its own callback.&#34;</span><span class="p">));</span>
</span></span><span class="line"><span class="cl"><span class="n">TGuardValue</span><span class="o">&lt;</span><span class="kt">bool</span><span class="o">&gt;</span> <span class="n">Guard</span><span class="p">(</span><span class="n">bIsBusy</span><span class="p">,</span> <span class="nb">true</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// B. Programming error, but never take the game down: report (first time only) and skip.
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">ensureMsgf</span><span class="p">(</span><span class="o">!</span><span class="n">bIsBusy</span><span class="p">,</span> <span class="n">TEXT</span><span class="p">(</span><span class="s">&#34;Foo() re-entered; call ignored.&#34;</span><span class="p">)))</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="n">TGuardValue</span><span class="o">&lt;</span><span class="kt">bool</span><span class="o">&gt;</span> <span class="n">Guard</span><span class="p">(</span><span class="n">bIsBusy</span><span class="p">,</span> <span class="nb">true</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// C. Not an error: re-entry is ignored by design.
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="k">if</span> <span class="p">(</span><span class="n">bIsBusy</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="n">TGuardValue</span><span class="o">&lt;</span><span class="kt">bool</span><span class="o">&gt;</span> <span class="n">Guard</span><span class="p">(</span><span class="n">bIsBusy</span><span class="p">,</span> <span class="nb">true</span><span class="p">);</span></span></span></code></pre></div></div>
<p>B works in every configuration because <code>ensureMsgf</code> still evaluates its condition when <code>DO_ENSURE</code> is off; only the report goes away. C turns the guard into a game rule. If two characters both have Thorns and each reflects the full hit back at the attacker, damage would bounce between them until the stack overflows. With a per-character <code>if (bIsApplyingDamage) return;</code> at the top of the damage function, a character still processing a hit ignores the damage that bounces back, so the exchange stops after one reflection.</p>
<div class="callout callout--warning">
  <div class="callout__title">Warning</div>
  With A, Test and Shipping builds (by default) still set and restore the flag, but nothing checks it, so a re-entry there goes through unnoticed. If content can trigger re-entry (say, two items a designer happens to combine), use B or C.
</div>

<p>Sometimes the right answer is no guard at all:</p>
<ul>
<li>Recursion is intended. A tree walk that recurses through the same object would trip the guard on its first child.</li>
<li>The risk is another thread. A <code>bool</code> isn&rsquo;t a lock.</li>
<li>The nested call&rsquo;s work still has to happen. Defer it to the next pass, or iterate over a snapshot instead.</li>
</ul>
]]></content:encoded>
      
    </item>
    
  </channel>
</rss>
