This guide is the shortest useful tour of callonce. If you have not put the header on your include path yet, see Installation first.
The one type
callonce has a single public class template:
#include <memoized_invoke.hh>
using namespace fedem::utility;
memoized_invoke<ExecutionPolicy, F, Args...> mi;
You almost never write those template arguments — class template argument deduction fills them in from the constructor:
int expensive(int seed) { /* slow */ return seed * seed; }
memoized_invoke mi( &expensive, 7 ); // deduces <single_threaded, int(*)(int), int>
mi now owns the callable &expensive and the argument 7. It has not called anything yet.
Call it
int a = mi(); // runs expensive(7), caches 49, returns 49
int b = mi(); // returns the cached 49 — expensive() is NOT called again
The first operator() runs std::apply(callable, args), stores the result, and flips the object to done. Every later operator() returns the stored value. For a void callable there is nothing to return — it just runs once.
You can check the state and read the value without calling:
mi.is_done(); // true
mi.value(); // 49 (const reference; precondition: is_done() == true)
Change the arguments
The argument-taking overload compares the new arguments against the stored ones:
int c = mi(7); // same arg → cached 49, no re-run
int d = mi(9); // different arg → re-runs expensive(9), caches 81, returns 81
int e = mi(7); // different again → re-runs expensive(7)
There is exactly one argument slot. mi does not build a map from argument sets to results — switching back to 7 after 9 is a cache miss. If you want a full multi-key cache, that is a different data structure (and an explicit non-goal of this library — see the FAQ).
Reset
mi.reset(); // clear the cache, keep the argument, next call re-runs
mi.reset(42); // clear the cache AND replace the stored argument
reset() is what you call from a SIGHUP handler to force a config reload, or in a test between cases.
Choosing a policy
The default policy, single_threaded, has zero overhead and is not thread-safe. If several threads share one memoized_invoke and race on the first call, use lock_free via the factory:
auto shared = make_memoized<lock_free>( &connect_to_db );
// call shared() from many threads — connect_to_db runs exactly once
See Execution Policies for the full comparison.
A complete program
#include <memoized_invoke.hh>
#include <iostream>
using fedem::utility::memoized_invoke;
int slow_double(int x)
{
std::cout << "[compute] slow_double(" << x << ")\n";
return x * 2;
}
int main()
{
memoized_invoke d( &slow_double, 21 );
std::cout << d() << '\n'; // [compute] slow_double(21) \n 42
std::cout << d(21) << '\n'; // 42 (cached — no [compute] line)
std::cout << d(10) << '\n'; // [compute] slow_double(10) \n 20
d.reset();
std::cout << d() << '\n'; // [compute] slow_double(10) \n 20
}
What to read next
- Execution Policies —
single_threadedvslock_free, and how to write your own policy. - The Argument Cache — exactly what "the arguments changed" means, and the equality requirement.
- Exception Safety & Threads — what happens when the callable throws, and which operations are thread-safe.
- Reference › memoized_invoke — every member, with signatures.
- Examples — three worked programs.

