Defined in header <memoized_invoke.hh>
class lock_free;
lock_free is the synchronising ExecutionPolicy for memoized_invoke. It holds the run state in a std::atomic<execution_state> and makes the first-run race safe: exactly one thread runs the callable, and every other thread blocks until the result is published.
CTAD never selects it — construct with make_memoized<lock_free>(...).
How it works
| Step | Mechanism |
|---|---|
| Claim the run | try_enter() is one compare_exchange_strong from not_started to running (memory_order_acq_rel / acquire). The single thread that succeeds runs the callable. |
| Everyone else waits | wait() loads the state and, while it is running, parks on std::atomic<T>::wait (C++20) — futex-backed, no spin. |
| Publish success | mark_done() stores done with memory_order_release, then notify_all(). A thread that observes done through the matching acquire sees the cached value. |
| Publish rollback | mark_free() stores not_started with memory_order_release, then notify_all() — used when the callable throws or on reset(). Woken waiters re-enter and retry. |
Member functions
Same interface as single_threaded, every operation atomic:
| Function | Effect |
|---|---|
bool try_enter() | compare_exchange_strong not_started → running. true for the one winner. |
void mark_done() noexcept | Store done (release), notify_all(). |
void mark_free() noexcept | Store not_started (release), notify_all(). |
bool is_done() const noexcept | Acquire load == done. |
void wait() const noexcept | Block while the state is running; return when it becomes done or not_started. |
Special members — copy and move reset the state
lock_free() = default; // not_started
lock_free( lock_free const& ) noexcept; // -> not_started
lock_free( lock_free&& ) noexcept; // -> not_started
lock_free& operator=( lock_free const& ) noexcept; // -> not_started
lock_free& operator=( lock_free&& ) noexcept; // -> not_started
std::atomic is neither copyable nor movable, and a copy owns a separate synchronisation context — a "done" flag copied from another object would not be backed by that object's release store. So every copy and move re-initialises the state to not_started.
At the memoized_invoke level this means: a copied or moved memoized_invoke<lock_free, …> keeps the copied cached value but reports is_done() == false and re-runs the callable on its first call. See memoizedinvoke::memoizedinvoke.
What lock_free does not synchronise
Only the first-run race. reset() and the argument-changing operator()(args...) mutate the stored argument tuple and the cached-value optional, which are ordinary non-atomic members of memoized_invoke. Calling either concurrently with anything else on the same object is a data race. Quiesce all other threads before resetting a shared lock_free wrapper.
Example
#include <memoized_invoke.hh>
#include <atomic>
#include <thread>
#include <vector>
#include <cassert>
using fedem::utility::make_memoized;
using fedem::utility::lock_free;
int main()
{
std::atomic<int> runs{0};
auto once = make_memoized<lock_free>(
[&runs]{ runs.fetch_add(1); return 42; } );
std::vector<std::thread> ts;
std::vector<int> seen(16, 0);
for (int i = 0; i < 16; ++i)
ts.emplace_back([&, i]{ seen[i] = once(); });
for (auto& t : ts) t.join();
assert( runs.load() == 1 );
for (int v : seen) assert( v == 42 );
}
See also
- make_memoized — the way to construct with this policy.
- single_threaded — the zero-overhead alternative and the ExecutionPolicy interface.
- User Guide › Exception Safety & Threads — the concurrent exception-rollback path.

