6

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);
}
j00hi
  • 5,420
  • 3
  • 45
  • 82
  • Second is move semantic, i.e. different thing. https://stackoverflow.com/questions/3106110/what-is-move-semantics If you pass an argument by const reference, when you going to modify it inside function - object will be copied by copy constructor or copy assignment operator. When you are using move semantic - move constructor or move assignment operator will be in use. – Victor Gubin Jan 04 '21 at 12:57
  • 2
    No, the second is not necessarily move semantic, since it is a forwarding reference, which is a [conditional cast to r-value reference](https://youtu.be/IqVZG6jWXvs). I.e. only if the user provides an r-value reference, it will be an r-value reference. But actually, that's exactly my question: Is there actually any case where having the option to pass-in `func` as an r-value reference can have any advantage? I'm having a hard time coming up with an advantage of the forwarding-reference version (second) over the l-value-reference-only version (first). – j00hi Jan 04 '21 at 13:05
  • As a side-note: The official term for these references is forwarding reference now. The commitee didn't accept Scott Meyer's initial terminology here. – Secundi Jan 04 '21 at 13:50
  • 1
    @Secundi yes, that's true. But not everyone is happy with the official term. You can even spot some [rebel movement](https://youtu.be/TFMKjL38xAI?t=1397) now and then. :O – j00hi Jan 04 '21 at 14:00
  • 1
    @j00hi thanks! It's confusing even for non-beginners. Seems to be likely, the committee might change the wording again in future... – Secundi Jan 04 '21 at 14:19

3 Answers3

7

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)).

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
  • I haven't tried it, but I very much assume, that they fail in the _first_ case, when the reference to the lambda is const. – n314159 Jan 04 '21 at 13:21
  • You are right about `mutable` lambdas. Thanks. But then the question arises: Why not just pass `func` as non-const l-value reference? (Please see the edited question above.) – j00hi Jan 04 '21 at 13:29
  • Hmm, thanks for the input. These are some great remarks. I.e.is it safe to say that passing by forwarding-reference and by value would be the most generic/versatile solutions, but passing by forwarding-reference could be a bit more performant? – j00hi Jan 04 '21 at 13:39
  • @j00hi As noted, `std::ref(lambda)` or `std::cref(lambda)` passed by-value is basically pass-by-reference. So no, I see no evidence that passing by forwarding reference would be fundamentally more performant. Passing by value can permit the compiler to better understand where the state of the lambda is located (ie, it is local, so can be reasoned about easily), while by reference the compiler has to first prove nobody else has ability to modify that referenced-to state (harder). – Yakk - Adam Nevraumont Jan 04 '21 at 14:17
4

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.
n314159
  • 4,990
  • 1
  • 5
  • 20
  • Good point about the STL's patterns! However, I can see two versions commonly used in the STL: by value (as you have pointed out), but also by const l-value reference. – j00hi Jan 04 '21 at 14:31
0

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.

Secundi
  • 1,150
  • 1
  • 4
  • 12