LibFpp: Persistent STL-like C++ Library
- LibFpp is a C++ container library providing persistent iterators and STL-like containers that enable immutable, snapshot-based updates.
- It employs finger trees and optimized zippers to reconcile iterator expressiveness with persistent data structures while avoiding mutation hazards.
- The library achieves logarithmic complexity for most operations, offering safety and efficient update-capable traversal without in-place mutation.
Searching arXiv for the LibFPP paper and closely related terminology. LibFpp is a C++ container library that implements persistent iterators with value semantics and provides persistent counterparts to the Standard Template Library (STL), including vectors, maps, sets, strings, and related abstractions (Li et al., 15 Apr 2026). Its central objective is to reconcile two programming styles that are ordinarily separated: STL-style iterator-centric imperative programming over mutable containers, and persistent data structures in which updates create new versions while previous versions remain accessible. In LibFpp, a persistent iterator snapshots the version of its underlying container at creation, and iterator operations act on the iterator-local copy of the container. The result is an STL-like programming interface intended to eliminate iterator invalidation and aliasing hazards while retaining familiar cursor-based programming patterns (Li et al., 15 Apr 2026).
1. Concept and scope
LibFpp is presented as a library for persistent vectors, maps, sets, strings, and other abstractions as persistent counterparts to the Standard Template Library (STL) (Li et al., 15 Apr 2026). The design hierarchy described for the library treats these abstractions uniformly: a vector is a sequence; a multiset is a sorted vector; a set is a multiset with no duplicate elements; a multimap is a multiset of key-value pairs ordered by key; a map is a multimap with unique keys; and a string is a vector of characters under UTF-8.
The problem addressed is the tension between iterator expressiveness and mutation hazards. Ordinary STL iterators are tied to mutable containers and therefore inherit reference semantics. The paper emphasizes hazards including iterator invalidation, aliasing, data races / concurrent modification hazards, and subtle side effects from in-place mutation (Li et al., 15 Apr 2026). LibFpp replaces this model with persistent values. Variables may still be rebound, but operations create new versions rather than mutating shared state.
The library is intentionally STL-like in naming, general API shape, iterator idioms, and asymptotic complexity targets, but it is not positioned as a raw-performance substitute for array-backed mutable containers. The paper states that “LibFpp is not intended as a drop-in replacement for STL containers in performance-critical code.” This suggests a design point centered on safety, persistence, structural sharing, and expressiveness rather than maximal locality or pointer-arithmetic efficiency.
2. Persistent iterators and value semantics
The core abstraction is the persistent iterator. A persistent iterator is created from a container, for example through begin(), but unlike an STL iterator it is not merely a pointer-like reference into a mutable object. Instead, it is a value that includes both a cursor position and a snapshot of the referenced container version (Li et al., 15 Apr 2026).
This yields the library’s defining semantic property: iterator operations such as ++, erase, insert, and assignment-through-iterator act on the iterator-local persistent version. Mutating through one iterator does not mutate the original source container, and mutating through one iterator does not affect any other iterator. Previous versions remain accessible. The paper characterizes this explicitly as true value semantics.
The contrast with mutable STL code is central. In standard C++, erase-while-iterating requires careful reassignment because erase may invalidate the iterator. LibFpp instead permits update-capable traversal in a style that resembles STL but produces a new persistent value. A representative example is:
1 2 3 4 5 6 7 |
string filterAscii(string s) { string::iterator i; for (i = s.begin(); i != s.end(); ) if (*i >= 0x7F) i.erase(); else ++i; return i.value(); } |
Here the iterator evolves through successive persistent versions; s remains unmodified, and i.value() materializes the iterator’s current logical container (Li et al., 15 Apr 2026).
This design is also meant to address aliasing. In a mutable-reference model, two names may denote the same logical object, so updates through one name are visible through the other. In LibFpp, logical values are versioned. Physical structure may still be shared, but semantically visible mutation is absent. A plausible implication is that reasoning about local program state becomes closer to reasoning about immutable values, while preserving an imperative iterator idiom.
3. Data-structure foundation
LibFpp uses finger trees as the common implementation substrate for its containers (Li et al., 15 Apr 2026). The paper describes a finger tree as composed of recursive spine nodes, each with left and right finger nodes; fingers store 1–4 digits; digits are balanced 2–3 trees; and digit depth increases down the spine. The same representation underlies vectors, strings, sets, maps, multisets, and multimaps.
This uniformity is refined through invariants and subtree summaries. The paper states that each container is realized as a finger tree equipped with a specialized monoid summarizing subtree information. For sets and maps, the monoid is the minimum element of the left subtree; for strings, the monoid is the total Unicode length under the UTF-8 encoding (Li et al., 15 Apr 2026). This supports a single persistent substrate with specialized behavior for ordered lookup, sequence access, and UTF-8-aware string operations.
The stated performance properties of the finger-tree basis are: amortized O(1) adding and removing elements at the front or back; O(\log N) access, insertion, and deletion at arbitrary positions; and O(\log N) append and split (Li et al., 15 Apr 2026). Because the representation is persistent, updates only copy modified paths while structurally sharing unaffected subtrees.
The paper presents this as more than an implementation convenience. It enables “zero-cost upcasts” between related abstractions within the hierarchy. This suggests that the library’s container taxonomy is not a collection of unrelated wrappers, but a single persistent sequence framework specialized by ordering and summarization constraints.
4. Iterator implementation as optimized zippers
Persistent iterators are implemented as optimized zippers over finger trees (Li et al., 15 Apr 2026). A zipper stores a focused node together with the reversed path to the root. In LibFpp, the zipper node is described by the tuple
where tree is the finger-tree node in the path, child indicates which child is taken, dirty indicates whether that subtree has been modified relative to the parent, and parent is the next zipper node up the reversed path or null at the root (Li et al., 15 Apr 2026).
The iterator head points at the current element node, so dereference is direct. Navigation ascends to the lowest common ancestor of source and destination and then descends again. The paper states that local ++it and --it are amortized O(1), while larger jumps such as it += N are O(\log N).
A key implementation device is the dirty flag. Assignment through an iterator is described by allocating a new leaf node n, creating a new zipper head, and marking that head dirty. The paper gives the resulting transformation as
which yields assignment in O(1) (Li et al., 15 Apr 2026). Full reconstruction of enclosing tree nodes is deferred. When upward traversal or value extraction forces rebuilding, dirty flags propagate and affected nodes are rebuilt lazily.
Insertion and deletion follow the same general strategy: unwind to a nearby ancestor where balancing can be restored, rebuild a local balanced subtree, mark the relevant zipper node dirty, and defer full root reconstruction. The paper states that the unwinding depth is amortized O(1) (Li et al., 15 Apr 2026).
The operation value() materializes the iterator’s current container version by walking from the current zipper node to the root and rebuilding dirty nodes as needed. Its worst-case complexity is stated as O(\log N), and it is described as pure: it does not invalidate or mutate the iterator (Li et al., 15 Apr 2026).
5. API and operational behavior
LibFpp aims to support all of the standard C++ STL iterator operations (increment, decrement, dereference, etc.) with similar asymptotic complexity goals, subject to the constraints of a tree-based persistent representation (Li et al., 15 Apr 2026). The following operations are explicitly listed in the paper for representative container families.
| Container family | Representative operations | Reported character |
|---|---|---|
| Vector | size, operator[], front, back, push_back, push_front, pop_back, pop_front, begin, iterator insert, erase, assign, ++, --, *, += n |
Sequence-style persistent API |
| Set / Map | size, keyed access, indexed access, contains, insert, erase, begin, find, iterator erase, ++, --, *, += n |
Ordered persistent API |
| String | size, operator[], concatenation, push_front, substr, begin, iterator insert, erase, assign, ++, --, *, += n |
UTF-8 persistent sequence |
The most visible semantic divergence from STL is that iterator-local updates do not mutate the original container. Instead, programs often end by extracting the iterator’s current version via value(). This is also what makes persistent iterators suitable for snapshot-based workflows. The paper’s text-editor example uses a cursor type alias
1 |
using Cursor = string::iterator; |
and stores undo/redo history as vectors of cursors. Since a cursor simultaneously represents a container version and a position, snapshots become cheap shallow copies through structural sharing (Li et al., 15 Apr 2026).
The paper also uses a stateful filtering example to argue that persistent iterators support imperative, cursor-oriented patterns that are awkward with higher-order combinators. This suggests that LibFpp is aimed not only at persistence as a storage discipline, but at preserving a particular style of algorithm expression.
6. Complexity profile, performance, and engineering tradeoffs
The paper states that the complexity for any standard operation is no more than O(\log N), and that LibFpp achieves time and space complexities comparable to the STL across nearly all operations while adding full value semantics without complexity hazards (Li et al., 15 Apr 2026). The main asymptotic tradeoff is that tree-based persistent sequences cannot retain O(1) random indexing and pointer-like jumps.
For sequence containers, the paper reports \mathcal{O}(\log N) for indexed access and it += n, whereas STL vector and string provide \mathcal{O}(1). Conversely, several local iterator edits become asymptotically better than mutable array-backed containers: for vectors and strings, iterator-local insert and erase are reported as \mathcal{O}(1) in LibFpp but \mathcal{O}(N) in STL vector/string (Li et al., 15 Apr 2026). String concatenation is reported as \mathcal{O}(\min(\log N, \log M)) versus \mathcal{O}(N+M) for STL strings, and substr(idx) as \mathcal{O}(\log N) versus \mathcal{O}(N).
The evaluation uses a macOS arm64 M2 Pro system with 32 GB memory and Clang 21.1, comparing LibFpp against STL (libc++), Immer, Abseil, and Folly. Reported container-microbenchmark results versus STL include 56.9× slower vector access, 3.4× slower vector append, 262.0× slower vector update, 3.3× slower set access, 7.6× faster vector concat, and 1.8× faster set append (Li et al., 15 Apr 2026). Iterator microbenchmarks versus STL report 23.6× slower vector Access*, 11.8× slower vector Append*, 45.2× slower vector Update*, 28.2× slower vector Erase*, 3.1× slower set Access*, 2.9× faster set Append*, and 2.9× faster set Erase* (Li et al., 15 Apr 2026).
These measurements reflect the paper’s central tradeoff: zipper movement and tree traversal are substantially more expensive than pointer arithmetic on contiguous arrays, but modification-heavy node-based workloads can be competitive or faster. The paper summarizes the runtime picture by stating that “LibFpp achieves practical performance comparable to existing persistent libraries, while offering a more general and expressive persistent iterator design.”
Several engineering techniques are used to narrow the persistence overhead. The implementation uses reference counting for deterministic reclamation, justified by the acyclic nature of immutable trees (Li et al., 15 Apr 2026). It also introduces a fixed-size custom memory allocator (CMA) under the assumption that finger-tree and zipper nodes fit in one 64-byte cache line, with claimed 4 inlined instructions for allocation and 5 inlined instructions for deallocation. In addition, the library performs destructive update when safe—specifically, when all nodes along an update path have a reference count of exactly one—and packs multiple elements into each leaf node to improve locality and reduce tree depth (Li et al., 15 Apr 2026). This suggests a copy-on-write-like optimization strategy under uniqueness.
7. Relation to functional persistence and prior C++ practice
LibFpp is positioned between two established traditions. On one side are mutable STL containers and iterators, which are expressive but hazard-prone. On the other side are functional persistent data structures, which avoid mutation hazards but commonly rely on recursion and higher-order combinators such as map, filter, foldl, and traverse (Li et al., 15 Apr 2026).
The paper argues that these combinator-oriented interfaces are not always natural for stateful iteration, early exit, cursor-style editing, undo/redo, or algorithms best expressed as loops. It contrasts LibFpp’s filterAscii example with Haskell Seq code using index-based deletion, noting that the latter becomes O(N \cdot \log N) while the LibFpp iterator version is O(N) (Li et al., 15 Apr 2026). This suggests that the novelty is not persistence alone, but persistence combined with an efficient cursor abstraction.
Among C++ persistent libraries, the paper identifies Immer as the closest comparison. The stated differences are that Immer uses RRB trees and CHAMPs, exposes const iterators only, and makes persistence/transience explicit, whereas LibFpp uses finger trees uniformly and introduces update-capable persistent iterators with value semantics (Li et al., 15 Apr 2026). The paper further states: “As far as we are aware, LibFpp is the first implementation of zippers for finger trees and the first to use zippers to implement an STL-like iterator-like API.”
The resulting interpretation is that LibFpp is not merely an STL-inspired persistent container set. It is a semantic redesign of iterators themselves. Containers are persistent values implemented by finger trees; iterators are persistent cursor values implemented by zippers; local updates create new versions via structural sharing; and value() materializes the iterator’s current snapshot. The library’s contribution is therefore the combination of persistence, iterator-based expressiveness, and explicit elimination of iterator-invalidating mutation patterns (Li et al., 15 Apr 2026).