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-argumentoperator()()and withreset()/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)andco(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 returnsvoid. Nothing runs until the nextoperator().
Use reset(args...) when you want to stage a new argument set now and defer the (possibly expensive) work to later.

