callonce has its own site starting today, at callonce.fedem.eu. Like its sibling cparse, callonce is a small C++ library that has lived so far only as an internal dependency and a proposal draft. Giving it a public home is a step towards treating it as a component other people can pick up.
What callonce is
One header. One class template:
#include <memoized_invoke.hh>
using namespace fedem::utility;
memoized_invoke config( &load_config, path );
auto& a = config(); // reads and parses the file
auto& b = config(); // cached — no second read
std::call_once runs a callable once across threads and then throws the result away. callonce keeps it. It also lets you reset(), and it re-runs when the arguments change. You choose the synchronisation: single_threaded is a bare enum with no overhead; lock_free is an atomic compare-exchange plus C++20 atomic::wait, so exactly one thread runs the callable and the rest block until the value is ready.
That is the whole library. It is the reference implementation for a WG21 proposal, std::memoized_invoke — the proposal text ships in the source archive as docs/Proposal.md.
What is here today
- An installation guide — single-header drop-in,
add_subdirectory, a standalone CMake package, or Conan. - A User Guide covering the memoization model, the
single_threaded/lock_freedecision, the argument cache and its one-slot behaviour, and exception safety across threads. - A Reference Manual with one page per symbol, alongside the generated Doxygen tree — class, collaboration and call/caller graphs from the single header.
- Three worked examples: a lazy config loader, an argument-sensitive compute cache, and a 16-thread one-time initialiser. Each has a byte-exact regression test.
- The Downloads page — the
1.0.0source archive (.tar.gz/.zipwith SHA-256 sidecars), agit archivesnapshot at the tag.
A bug found on the way here
Building the examples and expanding the test suite surfaced a real concurrency bug in the library. Under lock_free, when the thread that won the run threw an exception, the state was rolled back correctly and the waiters were woken — but a woken waiter fell straight through without re-attempting the work. The next thing it did was read the still-empty cache, which threw std::bad_optional_access. That is unrelated to the exception the callable threw, so it escaped the worker's catch and called std::terminate.
The fix is small: the wait path is now a loop — re-check whether the value is ready, retry the compare-exchange, otherwise wait again. It matches what the documentation already claimed ("exception rollback also wakes waiting threads, allowing another thread to retry"), and there is now a 200-iteration regression test hammering exactly that race. It ships in 1.0.0.
What is next
- A
constexprpath for thesingle_threaded-only case —optionalandtupleare alreadyconstexprin C++20;atomicis the blocker, and it is not on that path. - Proposal iteration — feedback on the WG21 draft, especially around the argument-comparison-by-
operator!=design and whether a custom comparator belongs in scope.
If you use callonce, or the pattern it captures, in a project of your own, say hello via the contact page.

