Passing std::function by value VS by reference

I have been thinking about this for some days and haven't been able to get an answer for this.
What is the effect of passing std::function by value VS by reference in the code below?
What happens exactly when it is passed by reference?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <iostream>
#include <functional>
 
void invokeV1(const std::function<void(void)> fn) // # PASS LAMBDA BY value*********************
 
{
    fn();
}
 
void invokeV2(const std::function<void(void)>& fn) // # PASS LAMBDA BY REFERENCE*********************
 
{
    fn();
}
 
int main()
{
    int i{ 0 };
 
    // Increments and prints its local copy of @i.
    auto count{ [i]() mutable {
      std::cout << ++i << '\n';
    } };
 
    invokeV1(count);
    invokeV1(count);
    invokeV2(count);
    invokeV2(count);
 
    return 0;
}
Last edited on
What is the effect of passing std::function by value VS by reference in the code below?

Not much difference here.

The std::function bound to the reference parameter is a rvalue formed from an invocation of its converting constructor template. Basically, the lambda's copied each time either way.

std::function was designed to have value semantics;
Use std::ref if you want reference semantics instead.
invoke(std::ref(count));
Last edited on

Sorry. I don't get what you said. Would you please elaborate more or introduce books or resources so that I could understand what is exactly happening behind the scenes when std::function is passed to a function by reference VS by value.

Thank you.
Last edited on
Sorry. I don't get what you said

No worries, I gave a brief answer just to point out a subtlety.

Consider this simpler example:
1
2
3
4
5
void f(double const& x);
void g(double y);
int i = 0;
f(i);
g(i);

The type of i is int, not double. Therefore, when i is provided as an argument to f, the compiler must convert the argument from int to double.

This process is called implicit conversion. The result of the conversion is a value (specifically, a prvalue) with type double, which is used to initialize the parameter x.

A similar process occurs when passing i to g. An expression of type int was provided where a double was expected, so the compiler must convert int to double. The result of the conversion is used to initialize the parameter y.

The situation you've provided is similar:

The compiler writes a new class for each lambda expression that appears in the code - therefore, the type of count is not std::function<void(void)>. Instead, its type is unique - a compiler-generated class type that we'll call ClosureType.

When you pass count to invokeV1 (by-value), you've provided an argument with type ClosureType where a std::function<void(void)> was expected, so a conversion is required.

In order to do so, the compiler implicitly instantiates and calls std::function<void(void)>'s constructor template, which has the signature
template<typename F> function(F f);

The constructor that's finally called
a.) is non-explicit; and
b.) has one argument.
Such constructors are called converting constructors. Converting constructors are the only constructors suitable for use by the compiler during implicit conversions.

The constructor produces a std::function<void(void)> that wraps a copy of the function object count. The result is used to initialize the parameter fn. When fn's finally called, it doesn't affect the original object at all. Note that the copy of count is made by std::function's constructor during the implicit conversion, not by your code.

When you pass count to invokeV2 (by-reference), the situation's the same as the last paragraph.

If you declared count as
1
2
3
 std::function<void(void)> count{ [i]() mutable {
      std::cout << ++i << '\n';
    } };
then no conversion step is required - so no copy is made unless you pass by value. If you pass count by reference, the changes to the state of count will be reflected throughout the program.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
#include <iostream>
#include <functional>

using fn_type = std::function<void(void)>;

void call_std_fn_passed_by_const_reference  (fn_type const& fn)
{ std::cout << "std::function const&: "; fn(); }
void call_std_fn_passed_by_mutable_reference(fn_type& fn)       
{ std::cout << "std::function&:       "; fn(); }
void call_std_fn_passed_by_value            (fn_type fn)        
{ std::cout << "std::function:        "; fn(); }
template <typename Fn> void call_fn_by_value(Fn fn)             
{ std::cout << "F:                    "; fn(); }
  
int main()
{
  {
    fn_type fn { [i=0]() mutable { std::cout << (++i) << '\n'; } };
    
    call_std_fn_passed_by_const_reference(fn);
    call_std_fn_passed_by_mutable_reference(fn);
    call_std_fn_passed_by_value(fn);
    call_fn_by_value(fn);
    std::cout << '\n';
  }
    
  { 
    auto fn { [i=0]() mutable { std::cout << (++i) << '\n'; } };
    
    call_std_fn_passed_by_const_reference(fn);
    // error: result of conversion is not an lvalue
    // call_std_fn_passed_by_mutable_reference(fn); 
    call_std_fn_passed_by_value(fn);
    call_fn_by_value(fn);
    std::cout << '\n';
  }  
  
  { 
    fn_type fn { [i=0]() mutable { std::cout << (++i) << '\n'; } };
    
    call_std_fn_passed_by_const_reference(std::ref(fn));
    call_std_fn_passed_by_mutable_reference(std::ref(fn));
    call_std_fn_passed_by_value(std::ref(fn));
    call_fn_by_value(std::ref(fn));
    std::cout << '\n';
  }

  { 
    auto fn { [i=0]() mutable { std::cout << (++i) << '\n'; } };
    
    call_std_fn_passed_by_const_reference(std::ref(fn));
    // error: not an lvalue
    // call_std_fn_passed_by_mutable_reference(std::ref(fn)); 
    call_std_fn_passed_by_value(std::ref(fn));
    call_fn_by_value(std::ref(fn));
    std::cout << '\n';
  }  
}
http://coliru.stacked-crooked.com/a/9d49af2642b10d5f
Last edited on
a nitpick, "// # PASS LAMBDA BY" is misleading; those functions are taking std::functions, not the lambdas themselves. If invokeV1/invokeV2 were templates, they would be taking lambdas directly.
using C++20 syntax, because now we can,
1
2
3
4
5
6
7
8
void invokeV1(auto fn) // # PASS LAMBDA BY value*********************
{
    fn();
}
void invokeV2(auto& fn) // # PASS LAMBDA BY REFERENCE*********************
{
    fn();
}

(the rest the same; live demo here https://wandbox.org/permlink/5STfCFSrKjpghRtd )
Topic archived. No new replies allowed.