memoized_invoke stores one argument set and one cached result. This page is about the rules that connect them.

One slot, not a map

memoized_invoke fib( &compute_fib, 10 );
fib();          // computes fib(10)
fib(20);        // computes fib(20)  — the stored arg is now 20
fib(10);        // computes fib(10) AGAIN — 10 is no longer the stored arg

There is a single argument slot. memoized_invoke is a memoized call, not a memoization table. If you need fib(10) and fib(20) to both stay cached, keep two memoized_invoke objects, or use a std::map<std::tuple<Args...>, R> — building that map is deliberately out of scope (see the FAQ).

How "changed" is decided

operator()(args...) builds a tuple from the new arguments and compares it with the stored tuple using operator!=:

if (stored_args != std::tuple(new_args...)) { /* re-run */ }

So the comparison is element-wise == on the whole tuple. Consequences:

  • Every argument type must be equality-comparable. A type with no operator== cannot be used with the argument-taking overload. It can still be used with the zero-argument operator()() and with reset() / reset(args...).
  • Comparison is by value, using each type's own operator==. For floating-point arguments that means exact bit equality — co(0.1 + 0.2) and co(0.3) are "different".
  • For pointer arguments (including the object pointer of a member function), identity is pointer equality, not pointee equality.

Arguments are stored decayed

The stored tuple is std::tuple<std::decay_t<Args>...>. References, const and array-to-pointer all decay:

memoized_invoke greet(
    [](std::string const& name){ return "hi " + name; },
    std::string("ada") );     // stores std::string, not std::string const&

operator() and reset() take std::decay_t<Args>... by value too, so the call site always passes owned values. For a large argument you do not want to copy on every call, wrap it in std::reference_wrapper — the same pattern std::bind and std::thread use — and accept that you are now responsible for the referent outliving the memoized_invoke.

The zero-argument overload never compares

memoized_invoke co( &f, a, b );
co();     // if is_done(): return cache. else: run f(a, b) with the STORED args.

operator()() does not look at arguments at all — it runs with whatever is in the slot. Use it when the arguments were fixed at construction and never change, or when the argument types are not equality-comparable.

Member functions: the object is an argument

For a member function pointer, the object pointer is Args[0]:

Widget w1, w2;
memoized_invoke m( &Widget::measure, &w1, 5 );
m();                 // w1.measure(5)
m(&w1, 5);           // cached
m(&w2, 5);           // re-runs — different object pointer

reset(args...) versus operator()(args...)

Both replace the stored argument set. The difference:

  • co(args...) replaces the args and immediately runs (if they changed or it was not done) and returns the result.
  • co.reset(args...) replaces the args, clears the cache, returns to not-started, and returns void. Nothing runs until the next operator().

Use reset(args...) when you want to stage a new argument set now and defer the (possibly expensive) work to later.