To parametrize an algorithm with a function, you can use one of the following techniques:
- A function pointer
- A typed-erased function wrapper such as
std::function
(C++11) or std::move_only_function
(C++23)
- A template parameter
S5205 explains why using function pointers is an inferior solution.
Working with typed-erased function wrappers
Thanks to type erasure, std::function
is very flexible to use. You can store it in variables, including containers such as
std::map<std::string, std::function<void(void)>>
. In other words, std::function
can represent any kind of
functor, including lambdas, as long as their signatures are compatible.
std::move_only_function
is very similar to std::function
. The main difference is that, as its name implies, it cannot be
copied and has to be moved. You can use it to store a lambda capturing a non-copyable object such as std::unique_ptr
, which cannot be
done with std::function
.
The abstraction offered by std::function
and std::move_only_function
comes at a cost: a compiler typically cannot inline
a call to these types due to the type erasure.
Additionally, these wrappers can be "empty", meaning they do not currently represent any callable. While invoking an empty
std::function
throws a std::bad_function_call
, invoking an empty std::move_only_function
results in undefined
behavior.
Working with template-parameters
Template parameters are less flexible than the type-erased wrapper: Each functor has its own type, which prevents storing different parameters
together in a container even if they all have compatible signatures.
On the other hand, since each template instance knows the type of the functor, calls can be inlined, making this a zero-cost abstraction.
Additionally, template parameters representing lambdas cannot be "empty". Therefore, by construction, there is no risk of undefined behavior or the
need to handle std::bad_function_call
when invoking such parameters.
Furthermore, C++20 concepts, such as std::predicate
or std::regular_invocable
, can enforce expected signatures and
provide useful compile-time error messages when incorrect functors are passed to your parametrized function.
Which solution to choose?
In conclusion, if a functor is known at compile-time, you should prefer using a template parameter; if it has to be dynamic, a typed-erased
function wrapper gives you greater flexibility.
This rule detects function parameters of type std::function
and std::move_only_function
that can likely benefit from
being replaced by a template parameter. It does so by looking at whether the functor is only called inside the function or if it participates in other
operations.
Exceptions
This rule ignores virtual functions because they don’t work well with templates.