<divclass="textblock"><p>We implement a <em>make-style</em><em>incremental</em><em>build</em><em>system</em> as a <aclass="el" href="classtf_1_1Taskflow.html" title="class to create a taskflow object">tf::Taskflow</a> that uses condition tasks to skip up-to-date targets and recompile only what is stale. This example demonstrates how condition tasks express data-dependent branching in a structurally fixed graph, and how to apply the <em>auxiliary</em><em>join</em><em>task</em> pattern to avoid the task race that arises at every multi-predecessor join point in the build graph.</p>
<p>A build system such as <code>make</code> or <code>ninja</code> maintains a directed acyclic graph of <em>targets:</em> source files, object files, libraries, and final binaries. Each directed edge means "this target depends on that file." When a build is requested, the build system traverses the graph and recompiles only the targets whose inputs have changed since they were last built — the target is <em>stale</em>. Targets whose inputs are all newer than the target are silently skipped.</p>
<p>The staleness check is a simple timestamp comparison: if any input file has a modification time newer than the target, the target must be rebuilt. This conditional skip-or-rebuild decision is exactly what condition tasks are designed to express — a task that inspects runtime state and routes the scheduler down one of two paths.</p>
<h1><aclass="anchor" id="MakeGraphProblem"></a>
A Concrete Build Graph</h1>
<p>Consider a small C project with three translation units and a single binary:</p>
</div><!-- fragment --><p>The dependency graph is:</p>
<divclass="dotgraph">
<iframescrolling="no" frameborder="0" src="dot_make_dag.svg" width="360" height="336"><p><b>This browser is not able to show SVG: try Firefox, Chrome, Safari, or Opera instead.</b></p></iframe></div>
<p>Source and header files (blue) have no dependencies and always exist on disk. Object files (yellow) depend on their source and included headers. The final binary (green) depends on all three object files. <code>main.o</code>, <code>util.o</code>, and <code>math.o</code> have no mutual dependency and can be processed simultaneously on separate cores.</p>
<p>Each object file target has two possible outcomes at runtime: either it is up-to-date (skip compilation) or it is stale (run the compiler). This is a binary branch — the natural role for a condition task. Each condition task checks the timestamps of its inputs against its output and returns <code>0</code> to skip or <code>1</code> to rebuild.</p>
<p>Condition tasks fit naturally here because the <em>graph</em><em>structure</em> is entirely static — it is determined by the build rules, not by file contents — while the <em>routing</em><em>decision</em> is dynamic, determined at runtime by timestamp comparison. Static tasks handle the fixed structure; condition tasks handle the dynamic branch.</p>
<h1><aclass="anchor" id="MakeGraphPitfall"></a>
The Task Race at the Join Point</h1>
<p>A first attempt might wire the condition tasks directly to <code>app:</code> each condition task returns <code>1</code> to run the compile step, which then feeds <code>app</code> with a strong edge, and returns <code>0</code> to route directly to <code>app</code> via a weak edge, skipping compilation.</p>
<divclass="dotgraph">
<iframescrolling="no" frameborder="0" src="dot_make_wrong.svg" width="626" height="523"><p><b>This browser is not able to show SVG: try Firefox, Chrome, Safari, or Opera instead.</b></p></iframe></div>
<p>This graph has a fatal flaw. Task <code>app</code> sits at the junction of three condition tasks' outputs. All three condition tasks can complete simultaneously — if all three find their object files stale, all three fire their <code>"1 (rebuild)"</code> branch and the three compile tasks each try to satisfy <code>app's</code> strong dependency counter concurrently. This is the correct path. But if some condition tasks return <code>0</code> (skip), they schedule <code>app</code> directly through a weak edge at the same time that other compile tasks are also converging on <code>app</code> via strong edges. <code>app</code> can be scheduled more than once, which is undefined behaviour.</p>
<p>This is precisely <b>Pitfall</b><b>2</b> (Task Race) from <aclass="el" href="ConditionalTasking.html#AvoidCommonPitfalls">Avoid Common Pitfalls</a> where a task sitting at the convergence of multiple condition task outputs is at risk of being scheduled concurrently by different paths.</p>
<h1><aclass="anchor" id="MakeGraphJoinTask"></a>
The Auxiliary Join Task Pattern</h1>
<p>The fix is to insert one <em>join</em> task per object file between the condition task and <code>app</code>. Each join task is a lightweight no-op that serves as a controlled merge point for the skip and rebuild paths of a single object file. <code>app</code> then has exactly three strong dependencies — one per join task — and is enqueued exactly once, after all three join tasks have completed.</p>
<divclass="dotgraph">
<iframescrolling="no" frameborder="0" src="dot_make_correct.svg" width="788" height="450"><p><b>This browser is not able to show SVG: try Firefox, Chrome, Safari, or Opera instead.</b></p></iframe></div>
<divclass="line"> --1 (rebuild)-> cc obj.o --> obj.join</div>
<divclass="line">obj.join --> app</div>
</div><!-- fragment --><p>The join task has one weak dependency (from the condition task on the skip path) and one strong dependency (from the compile task on the rebuild path). Exactly one of these two paths activates the join task on any given run, so it is scheduled exactly once. The following table lists the strong and weak dependency counts for all tasks in the graph:</p>
<p>We represent each build target as a <code>Target</code> struct carrying its output path, input paths, and compile command. The staleness check compares modification times; condition tasks return the result. Join tasks are plain no-op lambdas whose only purpose is to serve as the controlled merge point described above.</p>
<divclass="ttc" id="aclasstf_1_1Executor_html"><divclass="ttname"><ahref="classtf_1_1Executor.html">tf::Executor</a></div><divclass="ttdoc">class to create an executor</div><divclass="ttdef"><b>Definition</b> executor.hpp:62</div></div>
<divclass="ttc" id="aclasstf_1_1Executor_html_a519777f5783981d534e9e53b99712069"><divclass="ttname"><ahref="classtf_1_1Executor.html#a519777f5783981d534e9e53b99712069">tf::Executor::run</a></div><divclass="ttdeci">tf::Future< void > run(Taskflow &taskflow)</div><divclass="ttdoc">runs a taskflow once</div></div>
<divclass="ttc" id="aclasstf_1_1Task_html"><divclass="ttname"><ahref="classtf_1_1Task.html">tf::Task</a></div><divclass="ttdoc">class to create a task handle over a taskflow node</div><divclass="ttdef"><b>Definition</b> task.hpp:569</div></div>
<divclass="ttc" id="aclasstf_1_1Task_html_a331b1b726555072e7c7d10941257f664"><divclass="ttname"><ahref="classtf_1_1Task.html#a331b1b726555072e7c7d10941257f664">tf::Task::succeed</a></div><divclass="ttdeci">Task & succeed(Ts &&... tasks)</div><divclass="ttdoc">adds precedence links from other tasks to this</div><divclass="ttdef"><b>Definition</b> task.hpp:1313</div></div>
<divclass="ttc" id="aclasstf_1_1Task_html_a8c78c453295a553c1c016e4062da8588"><divclass="ttname"><ahref="classtf_1_1Task.html#a8c78c453295a553c1c016e4062da8588">tf::Task::precede</a></div><divclass="ttdeci">Task & precede(Ts &&... tasks)</div><divclass="ttdoc">adds precedence links from this to other tasks</div><divclass="ttdef"><b>Definition</b> task.hpp:1305</div></div>
<divclass="ttc" id="aclasstf_1_1Taskflow_html"><divclass="ttname"><ahref="classtf_1_1Taskflow.html">tf::Taskflow</a></div><divclass="ttdoc">class to create a taskflow object</div><divclass="ttdef"><b>Definition</b> taskflow.hpp:64</div></div>
</div><!-- fragment --><p>On a fully clean build the expected output is:</p>
<divclass="fragment"><divclass="line">[build] cc main.o</div>
<divclass="line">[build] cc util.o</div>
<divclass="line">[build] cc math.o</div>
<divclass="line">[build] link app</div>
</div><!-- fragment --><p>After touching only <code>util.c</code> and rebuilding:</p>
<divclass="fragment"><divclass="line">[build] cc util.o</div>
<divclass="line">[build] link app</div>
</div><!-- fragment --><p><code>main.o</code> and <code>math.o</code> are skipped because their condition tasks return <code>0</code> and route directly to their join tasks, bypassing compilation entirely. <code>app</code> runs its own link check and finds <code>util.o</code> newer than <code>app</code>, so it relinks.</p>
<li>Condition tasks express the skip-or-rebuild branch cleanly: The decision of whether to recompile is made at the condition task, not scattered through the task bodies. The condition task queries the filesystem and routes the scheduler; the compile task only compiles. This separation keeps each task's responsibility narrow and makes the graph readable: a diamond node in the dump output signals a binary routing decision, and its two outgoing dashed edges show exactly what each outcome triggers.</li>
<li>The join task is the canonical fix for Pitfall 2 at build join points: Without join tasks, <code>app</code> sits at the convergence of three condition task outputs and can be scheduled up to three times simultaneously. The join task absorbs this convergence: it has one weak incoming edge (from the skip path) and one strong incoming edge (from the compile path), so exactly one path activates it per run. <code>app</code> then has only strong incoming edges and is enqueued exactly once. This is the <em>auxiliary</em><em>task</em> pattern described in <aclass="el" href="ConditionalTasking.html#AvoidCommonPitfalls">Avoid Common Pitfalls</a>, applied systematically at every join point in the build graph.</li>
<li><code>taskflow.dump()</code> makes the routing explicit: Calling <code>taskflow.dump(std::cout)</code> before running the executor emits a Graphviz description of the full graph, including the dashed weak edges from condition tasks. Inspecting the strong and weak dependency counts of each task (see <aclass="el" href="ConditionalTasking.html#TaskSchedulingPolicy">Understand our Task-level Scheduling</a>) provides a quick sanity check: any task with multiple incoming weak edges from concurrently executable condition tasks is a potential race site, and should be given an auxiliary join task.</li>
<li>The link task performs its own staleness check: Unlike object file targets, the link step cannot be expressed as a condition task because it must always run <em>after</em> all three join tasks complete — its strong dependency count of three already guarantees the correct ordering. The staleness check inside <code>app's</code> lambda is a secondary guard that avoids re-linking when all three join tasks came through the skip path and no object file changed. This is consistent with how <code>make</code> behaves: the link rule runs its recipe only when its inputs are newer than its output.</li>
</ul>
<dlclass="section note"><dt>Note</dt><dd>This example models a three-target project for clarity. A real build system with hundreds of targets follows the same pattern: one condition task and one join task per non-leaf target, wired according to the project's dependency declarations. The number of tasks grows linearly with the number of targets, and the executor automatically exploits all available parallelism among independent targets in the same topological level. </dd></dl>
</div></div><!-- contents -->
</div><!-- PageDoc -->
</div><!-- doc-content -->
<!-- HTML footer for doxygen 1.13.1-->
<!-- start footer part -->
<divid="nav-path" class="navpath"><!-- id is needed for treeview function! -->
<ul>
<liclass="navelem"><aclass="el" href="Examples.html">Learning from Examples</a></li>
<liclass="footer">
Maintained by <ahref="https://tsung-wei-huang.github.io/">Dr. Tsung-Wei Huang</a>
—
Generated by <ahref="https://www.doxygen.org/index.html"><imgclass="footer" src="doxygen.svg" width="104" height="31" alt="doxygen"/></a> 1.13.1