Blog

callonce.fedem.eu is live

Posted on 2026-09-07

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

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 constexpr path for the single_threaded-only case — optional and tuple are already constexpr in C++20; atomic is 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.