Recommended reading
To understand std::forward
1 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::forward
1 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.