How is this different from std::call_once?

std::call_once guarantees a callable runs once across threads, then discards the return value, cannot be reset, and ignores arguments. callonce keeps the return value (value(), and operator() returns it), can be reset(), and re-runs when the arguments change. When you only need "run this once, ignore the result", std::call_once is smaller and you should use it — lock_free here is essentially std::call_once plus a std::optional and an argument tuple.

Why can't I use a generic lambda directly?

auto add = [](auto a, auto b){ return a + b; };
memoized_invoke co( add, 3, 4 );   // ❌ won't compile

A generic lambda has a templated operator() — there is no single signature for std::invoke_result_t to resolve. Wrap it in a std::function with a concrete signature first:

std::function<int(int,int)> add_fn(add);
memoized_invoke co( add_fn, 3, 4 );   // ✅

Can it cache more than one argument set?

No. There is one argument slot. co(10) then co(20) then co(10) computes 10 twice. If you need a real table, use std::map<std::tuple<Args...>, R> (or std::unordered_map with a tuple hash). A memoized_map is listed as a possible future extension in the proposal but is not in scope for this library.

Is it constexpr?

Not yet. std::optional and std::tuple are constexpr in C++20, but std::atomic is not, and the class is written for run-time use. A constexpr-friendly single_threaded-only path is a plausible future addition.

What compilers work?

Anything with C++20 std::atomic<T>::wait / notify_all: GCC ≥ 11, Clang ≥ 14, MSVC ≥ 19.29. The header uses no compiler extensions. The CI matrix for the library is GCC and Clang on Linux.

Does copying a memoized_invoke copy the cached result?

Yes — the cached value is copied. But with lock_free the execution state is reset to not-started on copy or move (std::atomic is not copyable), so a copied lock_free object re-runs on its first call even though it holds the copied value. single_threaded copies its state normally, so a copy of a done object serves the cache. See Execution Policies.

The callable has side effects — will they happen once?

Once per genuine execution. That means: once on the first call, once more after each reset(), and once more each time the arguments change. It does not mean "once ever" if you keep changing the arguments or resetting.

Is there a stable ABI?

callonce is header-only and template-heavy — there is no compiled library and therefore no ABI surface. Pin the version you build against (callonce/1.0.0 in Conan, or a vendored copy of the header) the same way you would any other header-only dependency.

Where's the proposal?

callonce is the reference implementation for a WG21 library proposal, std::memoized_invoke. The proposal text ships in the source archive as docs/Proposal.md; the blog tracks its status.

I found a bug / I use this in a project

The contact page reaches the author directly. Bug reports with a minimal reproducer are especially welcome — the exception-rollback fix in 1.0.0 came from exactly one such report against the sibling cparse project's test suite.