How to lazily instantiate a functor argument passed in a function when calling it under some condition in C++14?

111 Views Asked by At

Assume that I have a function that abstracts some error-handling logic, it has argument which is a functor. Inside it, it calls this functor conditionally.

The function looks like this:

void HandleError(int err_code, std::function<void()> error_logger) {
  //...
  if (condition) {
    error_logger();
  }
  //...
}

But now a caller needs to construct the error_logger (consider a lambda) before calling this function. It is a little bit not optimal.

How to lazily instantiate the error_logger only when it's called when the condition is true? Unfortunately, I can only use C++14. But boost library could be used.

2

There are 2 best solutions below

2
Roman On BEST ANSWER

You can remove std::function from interface, for example you want to guarantee, that there will be no heap memory allocation, you can write the code in this way, using the lambdas directly

template<typename LogFunc>
void HandleError(int err_code, LogFunc &&error_logger) {
//...
  if (condition) {
    error_logger();
  }
//...
}

HandleError( 0x10, [](){ // log } );

However there is no much sense to save the performance on std::function construction. Also there is an optimization, when the heap allocation is avoided (for ex. for gcc).

0
Nikos Athanasiou On

Lazy "creation" implies a "function that produces the end object". When employing such a technique, you need to make sure that it's worth the trouble and by that I mean:

  • The object being lazily instantiated is re-used. There's no point moving around an "object producing function" and conjure something into existence whenever you need it, only to end up with more instances than what you'd have if you were using a single object to begin with.
  • The "thingy" that lazily instantiates your object is cheaper than the data itself. Objects with "heavy" constructors are one such case and you'll often see a pattern in legacy code where constructor is split from resource acquisition/initialization to allow conditionally calling the "heavy" stuff.
  • The end object is initialized in a thread-safe manner

For the scope of this answer I'll show c++17 code, but should be easily portable to c++14 if you use boost::optional instead of its std counterpart while replacing std::invoke with simple function call syntax:

template <class Fn>
struct lazy
{    
    using value_type = std::invoke_result_t<Fn>;
    
    lazy(Fn fun) : _function(std::move(fun))
    {
    }
    
    value_type const& operator*() const
    {
        std::call_once(_once, [&] {
            _data.emplace(std::invoke(_function));
        });
        return *_data;
    }
    
private:
    Fn _function;
    mutable std::once_flag _once;
    mutable std::optional<value_type> _data;
};

With lazy,

  • the object having deferred "creation" is kept as an optional and only constructed upon "first use".
  • construction is wrapped in call_once to avoid data races when multiple threads access a lazy object, because even "reading" can trigger construction
  • the contained value is obtained (or created for the first access) by dereferencing the lazy object

The simple usage example is to show how this would work with a non callable class instance:

int main ()
{
    lazy obj([] {
        HugeObject ret;
        /* fill the huge object */
        return ret;
    });
   
   use_or_not(obj);
   ...
   use_or_not(obj);   
}

In the series of use_or_not calls, the huge object might or might not be instantiated. After the first use, the lazy obj will contain a HugeObject and use it onwards:

Demo

C++14 Demo

Your specific case is to "lazily generate a function object". Well good news, that is still an object and use of lazy doesn't differ much:

lazy obj([] {
    std::function<void()> ret;
    ret = [] { std::cout << "nada" << std::endl; };
        
    return ret;
});

Lazily generate function object Demo

Since function objects are often more lightweight (unless they have to encapsulate a lot of data) please make sure you adhere to the rules mentioned at the beginning of the answer.


This is the first topic in the C++ 2023 closing keynote. The lazy class is pretty much copied from there.