Search code examples
c++lambdac++17

How to pass an object's method to a lambda-function?


A complex "calculator" is currently invoked like this:

            result = calculator.calc(a, b, extra);

Unfortunately, it may sometimes fail due to unrelated changes happening on the server. Synchronizing with different departments is difficult -- retrying is easier...

To that end, I implemented a retry lambda-function:

    auto retry = [](const auto& func, auto... args) {
        int attempts = 5;
        for (;;) try {
            return func(args...);
        }
        catch (const std::system_error& e) {
            /* Don't retry in case of a system_error */
            throw;
        }
        catch (const exception& e) {
            if (attempts-- == 0)
                throw; /* Give up and rethrow */
            Logging::log(Logging::Level::Warning,
                "%s. Retrying %d more times.", e.what(), attempts);
            sleep(2);
        }
    };

The above compiles, but, as I try to use it:

            result = retry(calculator.calc, a, b, extra);

I get an error from clang: reference to non-static member function must be called. GNU c++ flags the same line with invalid use of non-static member function.

Indeed, the calc() is a non-static method of the Calculator-class.

How would I make use of my new lambda?


Solution

  • In the lambda, replace func(args...) with std::invoke(func, args...). Now your lambda supports member function pointers as callable, in addition to function pointer/references, lambdas, and other function objects.

    To pass the member function pointer, the syntax is:

    result = retry(&Calculator::calc, &calculator, a, b, extra);
    

    where Calculator is the class type of calculator. The extra argument &calculator is necessary to invoke the non-static member function on the specified object/instance of the class. std::invoke() knows how to handle it automatically.

    You need to be a bit careful here. Because you pass the arguments by-value, if you write calculator instead of &calculator you will accidentally call the member function on a copy of calculator.


    Generally, it would be better to pass the arguments by forwarding reference:

    auto retry = [](auto&& func, auto&&... args) {
    

    Then you can also call it as:

    result = retry(&Calculator::calc, calculator, a, b, extra);
    

    std::invoke() knows how to handle both pointers and references to the class instance when invoking member function pointers.

    Note however that, because you potentially call func() multiple times, you shouldn't std::forward the forwarding references into the call.


    Also, as a side note, if you want to keep retry generic, it is not enough to catch std::exception. There is no requirement that exceptions must be derived from std::exception. That's just a convention used in the standard library, and sometimes by other libraries.

    A fall-back catch block for all other exceptions should look like this:

    catch(...)
    {
        // do something
    }