To understand std::forward1 and when to use them2 see references.

clang-tidy check - cppcoreguidelines-missing-std-forward

Recently I worked on updating our llvm toolchain to version 17. This brought in some nice new static analysis checks from clang-tidy-17. One interesting check introduced is cppcoreguidelines-missing-std-forward, that enforces C++ core guideline F.19.

The guideline is pretty clear to understand for non-function types in template arguments. However, does it serve any purpose when template argument refers to a function/callable type? Consider the following example:

template <typename Function, typename ReturnType = std::invoke_result_t<Function>>
ReturnType wrapper(Function&& function)
{
    return function();
}

clang-tidy check asks to use std::forward1 for function argument - godbolt.

error: forwarding reference parameter 'function' is never forwarded inside the function body [cppcoreguidelines-missing-std-forward,-warnings-as-errors]
    6 | ReturnType wrapper(Function&& function)
      |                               ^
10 warnings generated.

Updating wrapper to forward function argument fixes the issue:

template <typename Function, typename ReturnType = std::invoke_result_t<Function>>
ReturnType wrapper(Function&& function)
{
    return std::forward<Function>(function)();
}

But in what cases does forwarding a function object help?

Calling the right overload of member function

C++11 onwards, class member functions can be qualified with ref-qualifier. During overload resolution, compiler chooses the right member function to call based on if object is rvalue/lvalue. See section Member functions with ref qualifier. Providing rvalue overload of few member functions of your class can be helpful for performance optimization, because rvalue overload can assume that class data is not going to be used after they are called. For example, std::stringstream::str method has a rvalue overload, see the third overload. This overload returns a string move-constructed from the underlying buffer, thus avoiding a potential memory allocation and copy. Another example is std::optional::operator* that returns rvalue reference to contained value inside optional, if we use rvalue overload.

Consider following example function object that uses std::stringstream::str in a function object:

struct function_object
{
    std::string operator()() const &
    {
        std::cout << "lvalue overload, needs to return copy of data from stringstream\n";
        return value.str();
    }

    std::string operator()() &&
    {
        std::cout << "rvalue overload, can move data from stringstream\n";
        return std::move(value).str();
    }

    std::stringstream value;
};

Note that the lvalue overload of function call operator () in above class cannot move value because caller would expect the function_object.value to be non-modified, whereas the rvalue overload can make use of rvalue overload of stringstream::str method because function_object.value will not be used anymore.

Consider following usage:

function_object fn_object{};
fn_object.value << "Store some data in stream";
// fn_object is xlvalue in next expression, so we expect to use `std::string function_object::operator()&&`
auto value = wrapper(std::move(fn_object));

If we don’t use std::forward on function input argument of wrapper function, we end up using lvalue overload of our function_object () operator, even if we expected to use rvalue function object overload! Example - godbolt.

Following clang-tidy check suggestion, we get the expected result - godbolt. This is because we pass rvalueness of function object correctly from the wrapper function.

Conclusion

Using std::forward for function objects does make the function call operator read a little different. So, pass function objects as forwarding references only if it makes sense and there are profiling results to prove performance benefits. That being said, if you decide to pass function objects as forwarding references, always use std::forward on function object to ensure you get the right overload. Also, use static analysis tools like clang-tidy which catch such issues and help improve code quality.

References

  1. std::forward
  2. C++ core guideline F.15