examples/lazy-config/ in the source tree. Policy: single_threaded.

A great many programs read a small config file once at startup and then consult it repeatedly. Wrapping the parse in a memoized_invoke makes "once" literal, gives you a reset() for a SIGHUP-style reload, and keeps the parsed map owned in one place.

The program

#include <memoized_invoke.hh>

#include <fstream>
#include <iostream>
#include <map>
#include <string>

using fedem::utility::memoized_invoke;

namespace
{
  using config_map = std::map< std::string, std::string >;

  std::string trim( std::string s )
  {
    auto const b = s.find_first_not_of( " \t\r" );
    auto const e = s.find_last_not_of( " \t\r" );
    return b == std::string::npos ? std::string{} : s.substr( b, e - b + 1 );
  }

  config_map parse_config( std::string const& path )
  {
    // The one visible side effect — it must appear exactly once per load.
    std::cout << "[parse] reading config file\n";

    config_map out;
    std::ifstream in( path );
    std::string   line;

    while( std::getline( in, line ) )
    {
      if( auto const hash = line.find( '#' ); hash != std::string::npos )
        line.erase( hash );

      auto const eq = line.find( '=' );
      if( eq == std::string::npos )
        continue;

      auto const key = trim( line.substr( 0, eq ) );
      auto const val = trim( line.substr( eq + 1 ) );
      if( ! key.empty( ) )
        out[ key ] = val;
    }
    return out;
  }
}  // namespace

int main( int argc, char** argv )
{
  if( argc != 2 )
  {
    std::cerr << "usage: lazy-config <file>\n";
    return 1;
  }

  std::string const  path = argv[ 1 ];
  memoized_invoke    config( &parse_config, path );

  std::cout << std::boolalpha
            << "before first call, is_done = " << config.is_done( ) << '\n';

  for( int pass = 1; pass <= 3; ++pass )
  {
    config_map const& cfg = config( );
    std::cout << "pass " << pass
              << ": host=" << cfg.at( "host" )
              << " port=" << cfg.at( "port" )
              << " (is_done = " << config.is_done( ) << ")\n";
  }

  config.reset( );
  std::cout << "after reset, is_done = " << config.is_done( ) << '\n';

  config_map const& reloaded = config( );
  std::cout << "reloaded retries=" << reloaded.at( "retries" ) << '\n';

  return 0;
}

Reading it

memoized_invoke config( &parse_config, path ); — CTAD deduces memoized_invoke<single_threaded, config_map(*)(std::string const&), std::string>. The path is stored (decayed to std::string); nothing has run yet, so is_done() is false.

The three-pass loop. config() on the first pass calls std::apply(parse_config, {path}), which prints [parse] reading config file, builds the map, and caches it. Passes two and three return the cached map — [parse] does not print again. The whole point.

config_map const& cfg = config();operator() returns the result_type (a config_map) by value. Binding it to a const& extends the temporary's lifetime to the reference's scope. If the map were large and this were a hot path you would call config.value() instead, which returns a reference into the object's own storage with no copy — but it has the precondition is_done() == true.

config.reset(); — clears the cached map and returns to not-started. The stored path is kept. The next config() re-reads the file — you can see the second [parse] line. This is the line you would call from a signal handler.

Trying it

The example's smoke test (examples/lazy-config/src/CMakeLists.txt) runs it against tests/fixture.conf:

# lazy-config demo — a tiny key = value file
host = db.internal
port = 5432
retries = 3          # trailing comments are stripped

and checks the transcript byte-for-byte:

before first call, is_done = false
[parse] reading config file
pass 1: host=db.internal port=5432 (is_done = true)
pass 2: host=db.internal port=5432 (is_done = true)
pass 3: host=db.internal port=5432 (is_done = true)
after reset, is_done = false
[parse] reading config file
reloaded retries=3

A second test runs it with no argument and pins that it prints a usage line and exits 1 rather than crashing on std::string(argv[1] == nullptr).

See also