<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Lloyal Labs]]></title><description><![CDATA[Lloyal Labs]]></description><link>https://blog.lloyal.ai</link><image><url>https://cdn.hashnode.com/res/hashnode/image/upload/v1761540049295/146988e4-319a-4f67-bdb6-916213a7d440.png</url><title>Lloyal Labs</title><link>https://blog.lloyal.ai</link></image><generator>RSS for Node</generator><lastBuildDate>Sat, 02 May 2026 20:17:49 GMT</lastBuildDate><atom:link href="https://blog.lloyal.ai/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[InlinedVector: Yet Another SBO Container (But with a good reason)]]></title><description><![CDATA[We weren’t trying to “beat” Abseil or Boost. We just couldn’t ship them.
On mobile, every dependency is a shipping decision. We needed a small-buffer-optimized vector for lots of small, short-lived collections (token buffers, transient state), withou...]]></description><link>https://blog.lloyal.ai/inlinedvector-yet-another-sbo-container-but-with-a-good-reason</link><guid isPermaLink="true">https://blog.lloyal.ai/inlinedvector-yet-another-sbo-container-but-with-a-good-reason</guid><category><![CDATA[C++]]></category><category><![CDATA[abseil]]></category><category><![CDATA[memory-management]]></category><category><![CDATA[ Edge AI]]></category><category><![CDATA[header only libraries]]></category><dc:creator><![CDATA[Lloyal]]></dc:creator><pubDate>Tue, 28 Oct 2025 17:42:12 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1761694114948/74faea5f-7fad-46f8-9755-222460b570bf.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>We weren’t trying to “beat” Abseil or Boost. We just couldn’t ship them.</p>
<p>On mobile, every dependency is a shipping decision. We needed a small-buffer-optimized vector for lots of small, short-lived collections (token buffers, transient state), without dragging in a big ecosystem. So we built a <strong>single-header, zero-dep</strong> vector with SBO and a couple of behaviours we couldn’t find elsewhere.</p>
<p>The surprise benefit: <strong>it handles</strong> <code>insert</code>/<code>erase</code> on types with <code>const</code> members—a case where <code>std::vector</code> and common SBOs run into assignment requirements during in-place shifts. Our approach uses rebuild-and-swap on heap paths, so it stays well-defined for non-assignable types while keeping performance within a few percent.</p>
<hr />
<h2 id="heading-constraints-why-we-built-it">Constraints (why we built it)</h2>
<ul>
<li><p><strong>Zero dependencies</strong> (mobile/NDK; keep the binary small)</p>
</li>
<li><p><strong>SBO first</strong> (most instances are tiny)</p>
</li>
<li><p><strong>Modern C++</strong> (C++17/20; <code>std::variant</code>, traits, clean state handling)</p>
</li>
<li><p><strong>Production hygiene</strong> (fuzz tests, sanitizers)</p>
</li>
<li><p><strong>Can move heap → inline again</strong> after temporary spikes (not all SBOs do this in every version)</p>
</li>
</ul>
<p>For what it’s worth: Abseil’s <code>InlinedVector</code> implements <code>shrink_to_fit()</code> that can move storage back into the inline buffer when size permits. That’s exactly the behaviour we wanted.</p>
<blockquote>
<p><strong>Note on Boost</strong>: Older advice says <code>small_vector</code> “stays on heap after a capacity change.” In recent Boost (e.g., 1.86), <code>shrink_to_fit()</code> <em>can</em> migrate back to the small buffer when <code>size()</code> allows. If you rely on this, check your Boost version and tests.</p>
</blockquote>
<hr />
<h2 id="heading-the-design-choice-that-made-it-simple">The design choice that made it simple</h2>
<p>Most SBO vectors hand-roll a tagged union:</p>
<pre><code class="lang-cpp"><span class="hljs-keyword">enum</span> State { Inline, Heap };
<span class="hljs-keyword">union</span> { InlineBuf inline_; HeapBuf heap_; };
</code></pre>
<p>We use:</p>
<pre><code class="lang-cpp"><span class="hljs-built_in">std</span>::variant&lt;InlineBuf, <span class="hljs-built_in">std</span>::<span class="hljs-built_in">vector</span>&lt;T, Alloc&gt;&gt;
</code></pre>
<p>That gets us:</p>
<ul>
<li><p>Straightforward state management</p>
</li>
<li><p>A defined <code>valueless_by_exception</code> state (and an easy recovery story)</p>
</li>
<li><p>A smaller, easier-to-audit implementation</p>
</li>
</ul>
<p>In practice the discriminator check gets optimised away on hot paths; we saw no practical overhead vs hand-rolled tags.</p>
<hr />
<h2 id="heading-why-const-members-are-usually-painfuland-how-we-avoid-it">Why <code>const</code> members are usually painful—and how we avoid it</h2>
<p>Middle inserts for <code>std::vector</code> (and similar containers) rely on assignment-based element shifting. That imposes <strong>“MoveInsertable / effectively MoveAssignable”</strong> requirements on <code>T</code>—which fall over when <code>T</code> has <code>const</code> data members. You get ill-formed code or UB if you try to force it.</p>
<p><strong>What we do instead:</strong> on heap paths we <strong>rebuild-and-swap</strong>:</p>
<ol>
<li><p>Allocate a new buffer with the final size.</p>
</li>
<li><p>Construct elements in order (before/insertion/after).</p>
</li>
<li><p>Swap into place.</p>
</li>
</ol>
<p>You pay the same big-O cost (O(n) moves/copies dominate either way), but you <strong>don’t</strong> require <code>MoveAssignable</code>. That keeps inserts/erases working for:</p>
<ul>
<li><p>Types with <code>const</code> members</p>
</li>
<li><p>Types with deleted assignment operators</p>
</li>
<li><p>Immutable domain objects with mutable payloads</p>
</li>
</ul>
<h3 id="heading-compatibility-note-important">Compatibility note (important)</h3>
<ul>
<li><p><strong>C++20</strong> explicitly enables the “destroy-and-reconstruct” idiom for replacing subobjects via <code>std::destroy_at</code> / <code>std::construct_at</code> because of updates in <em>basic.life</em>. If you insist on <code>const</code> members and still want assignment-like updates, that’s your standards-conforming tool. It’s boilerplate—but legal.</p>
</li>
<li><p><strong>C++17</strong> doesn’t give you that latitude for arbitrary non-trivial types; our container stays on the right side of the rules by <strong>avoiding</strong> assignment requirements in the first place for the operations that would need them.</p>
</li>
</ul>
<blockquote>
<p>If you’re following the debate: Arthur O’Dwyer’s “Don’t const all the things” is a good overview of why blanket-<code>const</code> can backfire. Our position isn’t “const everywhere”; it’s “if you <em>do</em> have non-assignable types, the container shouldn’t make them unusable.”</p>
</blockquote>
<hr />
<h2 id="heading-the-other-picky-details-we-cared-about">The other picky details we cared about</h2>
<ul>
<li><p><strong>Allocator-aware</strong>, including inline elements (not just heap storage)</p>
</li>
<li><p><strong>Heap ↔ inline</strong> transitions via <code>shrink_to_fit()</code> when size permits (same spirit as Abseil)</p>
</li>
<li><p><code>std::variant</code> recovery: if an operation ever leaves the variant valueless, the next mutation recovers cleanly.</p>
</li>
<li><p><strong>Trivial-type fast paths</strong>: for implicit-lifetime types, byte moves are standards-conforming (see implicit-lifetime rules). <a target="_blank" href="https://en.cppreference.com/w/cpp/named_req/ImplicitLifetimeType.html">Cppreference</a></p>
</li>
</ul>
<hr />
<h2 id="heading-where-this-is-a-win">Where this is a win</h2>
<ul>
<li><p>Mobile/embedded where you <strong>can’t</strong> bring Abseil/Boost/LLVM along</p>
</li>
<li><p>Hot paths with <strong>tiny vectors</strong> (the usual SBO sweet spot)</p>
</li>
<li><p>Codebases that <strong>intentionally</strong> model immutability in some fields</p>
</li>
</ul>
<p>Where it’s <strong>not</strong>:</p>
<ul>
<li><p>You already depend on Abseil/Boost—use what you’ve got</p>
</li>
<li><p>Your vectors are typically large—then just use <code>std::vector</code> and reserve</p>
</li>
<li><p>You’re chasing the last 1% for complex inline types—Abseil may edge ahead there</p>
</li>
</ul>
<hr />
<h2 id="heading-on-dont-const-members">On “don’t <code>const</code> members”</h2>
<p>Some folks argue you should rarely (or never) use <code>const</code> data members. The core reasons: it complicates assignment/move semantics and can block otherwise straightforward container operations. Those are real trade-offs. Our view is narrower:</p>
<ul>
<li><p>If your <strong>domain model</strong> benefits from immutability (e.g., IDs), the container shouldn’t force you to redesign your type just to call <code>insert</code>.</p>
</li>
<li><p>If you don’t need <code>const</code> members—great. Most code doesn’t. But if you <em>do</em> choose them, the data structure shouldn’t be the thing that breaks.</p>
</li>
</ul>
<p>Pointers for further reading:</p>
<ul>
<li><p><a target="_blank" href="https://www.reddit.com/r/cpp/comments/icw0gk/the_implication_of_const_or_reference_member/">The implication of const or reference member variables in C++</a> (Reddit discussion)</p>
</li>
<li><p><a target="_blank" href="https://quuxplusone.github.io/blog/2022/01/23/dont-const-all-the-things/">“Don’t const all the things” (Arthur O’Dwyer) — the pitfalls of over-const.</a></p>
</li>
<li><p><a target="_blank" href="https://www.sandordargo.com/blog/2020/11/11/when-use-const-2-member-variables">When to use const in C++? Part II: member variables (Sandor Dargo)</a></p>
</li>
</ul>
<hr />
<h2 id="heading-quick-refs-you-might-care-about">Quick refs you might care about</h2>
<ul>
<li><p><code>std::vector</code> insert requirements (why non-assignable types run into trouble).</p>
</li>
<li><p><code>std::variant::valueless_by_exception</code> (and why we recover cleanly).</p>
</li>
<li><p><strong>Abseil</strong> <code>InlinedVector::shrink_to_fit</code> (moving back to inline).</p>
</li>
<li><p><strong>Boost</strong> <code>small_vector</code>: recent versions can return to the small buffer via <code>shrink_to_fit()</code>—check your Boost, don’t rely on dated advice.</p>
</li>
<li><p><strong>Implicit-lifetime types</strong> (why <code>memcpy</code> is valid for trivial types). <a target="_blank" href="https://en.cppreference.com/w/cpp/named_req/ImplicitLifetimeType.html">Cppreference</a></p>
</li>
</ul>
<hr />
<p>If your constraints look like ours, grab it and see if it fits:<br /><strong>Repo:</strong> <a target="_blank" href="https://github.com/lloyal-ai/inlined-vector">lloyal-ai/inlined-vector</a> • <strong>License:</strong> MIT<br /><strong>Vcpkg:</strong> <a target="_blank" href="https://vcpkg.roundtrip.dev/ports/lloyal-ai-inlined-vector">lloyal-ai-inlined-vector</a></p>
<p>Bugs, critiques, edge cases we missed—please open an issue. We’re building this in the open and happy to adjust when the evidence says we should.</p>
]]></content:encoded></item></channel></rss>