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
}