问题
What is the best (i.e. most performant, most versatile) way to pass a lambda function as a parameter to a function which only uses (but does not store or forward) the lambda function?
Option 1: Is passing it by const-reference the way to go?
template <typename F>
void just_invoke_func(const F& func) {
int i = 1;
func(i);
}
Option 2: Or is it better to pass it as a forwarding reference (universal reference)?
template <typename F>
void just_invoke_func(F&& func) {
int i = 1;
func(i);
}
Does the latter have any advantage over the former?
Are there any hazards with one of the solutions?
Edit #1:
Option 3: As pointed out by Yakk - Adam Nevraumont, mutable
lambdas would not work with option 1. So, what about another version that takes a non-const l-value reference:
template <typename F>
void just_invoke_func(F& func) {
int i = 1;
func(i);
}
The question is: Does option 2 have any advantage over option 3?
Edit #2:
Option 4: Another version that takes the lambda by value has been proposed.
template <typename F>
void just_invoke_func(F func) {
int i = 1;
func(i);
}
回答1:
If you try the different ways of taking different lambdas, you can see, that const references cannot take mutable lambdas and mutable reference cannot take temporaries (so you cannot define the lambda on the spot).
Both taking by value and taking by universal reference work for all types of lambdas. But the universal reference is more universal (pardon the pun): If you have a lambda that you want to use multiple times but that has no copy-constructor, you cannot take it by value, since you would have to move it and cannot (or at least should not) use it after that.
All in all, I think universal reference is the easiest way to go.
ADDENDUM: Because you asked about performance in the comments: If passing by forwarding reference vs. by value makes a huge difference, you are probably doing something wrong. Function objects should generally be light-weight. If you have a lambda that takes significant effort to pass by value, you should redesign that.
Even if you can make it work using forwarding references and so on, this seems to be a major hassle and you can easily make a mistake and have costly copy operations. Even if you do that absolutely right and never have copies in your code, there are two places where they can happen:
- other people's code: Most people think of function objects a light-weight and therefore have no problems with passing them by value.
- The STL: Take a look at the algorithms header. All function objects that are passed there are taken by value (because the STL also assumes function objects to be light-weight). So if you use a heavy one anywhere with STL-algorithms (and as we all know: We should use them, whereever we can) you are in for a bad time.
回答2:
Lambdas with the mutable
keyword have a non-const operator()
, and fail to compile in the first case.
I see no reason to block mutable lambdas.
Rvalue lambdas fail to compile in the non-const lvalue reference case.
Honestly, consider taking lambdas by value. If the caller wants non local state they can capture by reference, or even pass in std::ref
/std::cref
wrapped lambdas (If you are doing recursive calls that are not tail-call optimizable, rvalue reference is probably wise).
template<class F>
void do_something1( F&& f );
template<class F>
void do_something2( F f );
A call to do_something1(lambda)
can be emulated by a call to do_something2(std::cref(lambda))
(if immutable) and do_something2(std::ref(lambda))
(if mutable).
Only if the lambda is both mutable and an rvalue does this not work; 9/10 times that is a bug (a mutable lambda whose state is disposed of is iffy), and in the 1/10 times remaining you can work around it.
On the other hand, you cannot mimic do_something2
with do_something1
as easily.
The advantage of do_something2
is that the compiler, while writing the body of do_something2
, can know exactly where all of the state of the lambda is.
[x = 0](int& y)mutable{y=++x;}
if this lambda is passed by-value, the compiler knows locally who has access to x
. If this lambda is passed by-reference, the compiler must understand both the calling context and all code that it calls in order to know if someone else has modified x
between any two expressions in its own code.
Compilers can usually optimize values better than they can external references.
As for the cost of the copy, lambdas are usually not expensive to move. If they are [&]
-capture, they are just a bunch of references state-wise. If they are value-capture, it is rare their state is huge and difficult to move.
In those cases, std::ref
or std::cref
removes that cost anyhow.
(std::ref
's operator()
passes-through to the lambda, so the function never need know it was passed std::ref(lambda)
).
回答3:
From a design point of view, I'd like to add a maybe controversial aspect. The committee decided to use the wording 'forwarding references' to emphasize the core meaning of them, see for instance
https://isocpp.org/files/papers/N4164.pdf
So using them in an effectively non-forwarding context (you only apply a function/functor call), looks a bit contradictionary to this fundamental meaning, at least if you want your methods' signature yielding a clean meaningful naming and type categorization about their actual parameter usage context. On the other hand, you are more flexible in using them if your function bodies might change frequently and real forwarding could arise likely in the future.
来源:https://stackoverflow.com/questions/65562986/c-best-practice-pass-use-only-not-stored-lambda-argument-to-function-by-con