Defined in header <memoized_invoke.hh>

explicit memoized_invoke( F func )                              // (1)
    requires ( sizeof...( Args ) == 0 );

explicit memoized_invoke( F func, std::decay_t< Args >... args ) // (2)
    requires ( sizeof...( Args ) > 0 );

memoized_invoke( memoized_invoke const& other )                 // (3)
    requires std::is_copy_constructible_v< F >;

memoized_invoke( memoized_invoke&& other ) noexcept(/* see Notes */); // (4)

Parameters

ParameterDescription
funcThe callable. Moved into the object.
argsThe argument set to invoke func with. Taken by value (already decayed) and moved into the stored tuple.
otherAnother memoized_invoke of the same type to copy (3) or move (4) from.

1) Nullary constructor

Participates in overload resolution only when sizeof...(Args) == 0. Constructs a wrapper over a callable that takes no arguments. The stored argument tuple is empty.

memoized_invoke a( []{ return 42; } );
memoized_invoke b( &initialise_logging );   // void()

2) Constructor with arguments

Participates only when sizeof...(Args) > 0. Stores args... by value as std::tuple<std::decay_t<Args>...>.

memoized_invoke c( &std::pow, 2.0, 10.0 );
memoized_invoke d( &Widget::resize, &widget, 800, 600 );  // object ptr is arg 0

3) Copy constructor

Copies the callable, the policy, the cached value and the arguments. Requires std::is_copy_constructible_v<F>.

  • With single_threaded the policy state is copied, so a copy of a done object is itself done and serves the cached value without re-running.
  • With lock_free the policy state is re-initialised to *not-started* — std::atomic is not copyable and a copy owns a distinct synchronisation context. The cached value is still copied, but is_done() on the copy is false until its first call.

4) Move constructor

Moves the callable, policy, cached value and arguments. Same lock_free state re-initialisation as the copy. noexcept when both std::is_nothrow_move_constructible_v<F> and std::is_nothrow_move_constructible_v<std::tuple<std::decay_t<Args>...>> hold.

<a id="assignment"></a>Assignment

memoized_invoke& operator=( memoized_invoke const& other )
    requires std::is_copy_assignable_v< F >;

memoized_invoke& operator=( memoized_invoke&& other ) noexcept(/* as (4) */);

Both are self-assignment safe (this != &other guard). Copy assignment requires std::is_copy_assignable_v<F>. The same policy-state rules as the constructors apply: lock_free resets, single_threaded copies/moves the state.

<a id="destructor"></a>Destructor

~memoized_invoke() = default;

Trivial in effect — the object owns only value members (the callable, the policy, an optional, a tuple). No thread is joined, no lock is released; destroying a memoized_invoke while another thread is inside its operator() is a data race, the same as for any other object.

Notes

There is no default constructor: a memoized_invoke is meaningless without a callable.

CTAD (deduction guides) makes the class-name form work without template arguments and always picks single_threaded. For lock_free, use make_memoized.

Example

#include <memoized_invoke.hh>
#include <cassert>

using namespace fedem::utility;

int main()
{
    int runs = 0;
    memoized_invoke src( [&runs]{ return ++runs; } );
    assert( src() == 1 );

    auto copy = src;              // single_threaded: state copied
    assert( copy.is_done() );
    assert( copy() == 1 );        // no re-run
    assert( runs == 1 );

    auto shared = make_memoized<lock_free>( [&runs]{ return ++runs; } );
    shared();                     // runs -> 2
    auto shared_copy = shared;    // lock_free: state reset
    assert( !shared_copy.is_done() );
    shared_copy();                // runs -> 3
}

See also