Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How does std::bind Results in calling the Copy Constructor Several Times

I have been trying to understand how std::bind works. So working up with different examples. Below is the sample program whose output i am unable to understand.

VERSION 1

class NAME
{
  public:
    void f()
    {
        std::cout<<"f"<<std::endl;
    }
    NAME()
    {
        std::cout<<"default constructor"<<std::endl;
    }
    NAME(const NAME&)
    {
        std::cout<<"copy constructor"<<std::endl;
    }
};
int main()
{
   std::cout << "Hello World" << std::endl; 
   NAME n;
   std::function<void ()> callable = std::bind(&NAME::f, n);
   
   
   return 0;
}

The output of the above version 1 is as follows:

Hello World
default constructor
copy constructor
copy constructor

I know that the argument passed will be copied, so the copy constructor should be called only one time but in the output above the copy constructor is called twice. Why/how is this happening? Is it because the new callable that is created using std::bind will be used to initialize another callable using std::function on the lhs?

VERSION 2

int main()
{
   std::cout << "Hello World" << std::endl; 
   NAME n;
   std::function<void ()> callable = std::move(std::bind(&NAME::f, n));
   return 0;
}

The output of VERSION 2 is as follows:

Hello World
default constructor
copy constructor
copy constructor
copy constructor

In the output above(for version 2) when i use std::move, why/how is the copy constructor called thrice?

VERSION 3

int main()
{
   std::cout << "Hello World" << std::endl; 
   NAME n;
   auto callable = std::bind(&NAME::f, n);
   return 0;
}

The output of version 3 is as follows:

Hello World
default constructor
copy constructor

In this case(version 3) why/how is the copy constructor called only once?

VERSION 4

int main()
{
   std::cout << "Hello World" << std::endl; 
   NAME n;
   auto callable = std::move(std::bind(&NAME::f, n));
   return 0;
}

The output of version 4 is as follows:

Hello World
default constructor
copy constructor
copy constructor

What happens in this case(version 4) when we use auto and std::move(), why/how is the copy constructor called twice.

PS: The program is executed on an online compiler: Online compiler Used

Edit: Upon reading the comments i have a further question/doubt:

Question 1

If i use auto callable_1 = std::bind(&NAME::f, n); and std::function<void ()> callable_2 = std::bind(&NAME::f, n); then are the types of callable_1 and callable_2 different? If yes, then what is the type deduced by auto for callable_1? Or is it that the type deduced by auto for callable_1 will be an unnamed class object just like for a lambda.

Question 2

I have one further question: As i did in my case, if i pass n instead of &n in std::bind then is the code legal? For example, if i write std::function<void ()> callable_1 = std::bind(&NAME::f, n); and auto callable_2 = std::bind(&NAME::f, n); then are both of these illegal? What if i passed ref(n) instead of n in both of these cases, then will they be illegal or legal?

Question 3

Are the statements auto callable = [&n]{n.f()}; and auto callable = std::bind(&NAME::f, cref(n)); equivalent(in functionality) and maybe in some other ways and is there any reason(advantage) to pick one over another apart from coding style?

Question 4

What if i write auto callable = std::bind(&NAME::f, 1_); callable(n); callable(std::ref(n));. Is the statement callable(n) illegal? And what about the statement callable(std::ref(n));.

like image 265
Anoop Rana Avatar asked Mar 02 '23 11:03

Anoop Rana


2 Answers

First of all, according to the rules for move constructors, no implicit move constructor is defined for the class NAME. Further, from the Notes here:

If only the copy constructor is provided, all argument categories select it (as long as it takes a reference to const, since rvalues can bind to const references), which makes copying the fallback for moving, when moving is unavailable.

So, whenever you use std::move you are eventually calling a copy constructor. This explains why Version 4 (respectively, Version 2) has an additional call to the copy constructor compared to Version 3 (respectively, Version 1).

Let us see the remaining copy constructors.

As you correctly pointed out, a copy constructor is called from passing second argument of std::bind. This account for the first call in all versions.

When you declare

 std::function<void ()> callable = std::bind(&NAME::f, n);

you are calling the constructor of std::function, passing a single parameter std::bind(&NAME::f, n) which is then, again, copied. This account for the second call of copy constructor of version 1, and the third call in version 2. Notice that mandatory copy elision does not apply here, because you are not passing a std::function object.

Finally, when you use

auto callable = std::bind(...)

you are declaring a variable which is of an unnamed type, and contains the result of the call to std::bind. No copy is involved in the declaration. This is why version 3 has one less call to copy constructor compared to version 1.

Answers to the additional questions

1.

The types of callable_1 and callable_2 are different. callable_2 is a std::function object, while callable_1 is an unspecified type, the result of std::bind. Also, it is not a lamda. To see this, you can run a code like

   auto callable_1 = std::bind(&NAME::f, n);
   std::function<void ()> callable_2 = std::bind(&NAME::f, n);
   // a generic lambda
   auto callable_3 = [&]() { n.f(); };
   std::cout << std::boolalpha;
   std::cout << std::is_bind_expression<decltype(callable_1)>::value << std::endl;
   std::cout << std::is_bind_expression<decltype(callable_2)>::value << std::endl;
   std::cout << std::is_bind_expression<decltype(callable_3)>::value << std::endl;

See it live on Coliru.

2.

As noted by @RemyLebeau, a strict interpretation of the Notes in the reference of std::bind

As described in Callable, when invoking a pointer to non-static member function or pointer to non-static data member, the first argument has to be a reference or pointer (including, possibly, smart pointer such as std::shared_ptr and std::unique_ptr) to an object whose member will be accessed.

would suggest that the code must be called with &n and a call with n would be illegal.

However, a call to operator() results in a std::invoke. From the reference of std::invoke we read (little reformatting by me):

If f is a pointer to member function of class T:

a) If std::is_base_of<T, std::decay_t<decltype(t1)>>::value is true, then INVOKE(f, t1, t2, ..., tN) is equivalent to (t1.*f)(t2, ..., tN)

b) If std::decay_t<decltype(t1)> is a specialization of std::reference_wrapper, then INVOKE(f, t1, t2, ..., tN) is equivalent to (t1.get().*f)(t2, ..., tN)

c) If t1 does not satisfy the previous items, then INVOKE(f, t1, t2, ..., tN) is equivalent to ((*t1).*f)(t2, ..., tN).

According to this, calling std::bind with n (case a)) or &n (case c)) should be equivalent (apart from the additional copy if you use n), because std::decay_t<decltype(n)> gives NAME and std::is_base_of<NAME, NAME>::value is true (see reference for std::is_base_of). Passing ref(n) corresponds to the case b), so again it should be correct and equivalent to the other cases (apart from the copies discussed above).

3.

Notice that cref gives you a reference wrapper to const NAME&. So you will not be able to call callable because NAME::f is not a const member function. In fact, if you add a callable(); the code does not compile.

Apart from this issue, if you use instead std::ref or if NAME::f is const, I do not see a fundamental difference between auto callable = [&n]{n.f()}; and auto callable = std::bind(&NAME::f, ref(n));. With these regards, notice that:

The arguments to bind are copied or moved, and are never passed by reference unless wrapped in std::ref or std::cref.

Personally, I find the lambda syntax much clearer.

4.

From the reference for std::bind, under operator() we read

If the stored argument arg is of type T, for which std::is_placeholder::value != 0 (meaning, a placeholder such as std::placeholders::_1, _2, _3, ... was used as the argument to the initial call to bind), then the argument indicated by the placeholder (u1 for _1, u2 for _2, etc) is passed to the invokable object: the argument vn in the std::invoke call above is std::forward(uj) and the corresponding type Vn in the same call is Uj&&.

Thus, the effect of using placeholders falls back to the cases of question 1. The code indeed compiles with different compilers.

like image 83
francesco Avatar answered Apr 30 '23 12:04

francesco


// default constructor will be called
NAME n; 

// bind makes a copy of n into an internal struct, copy constructor of n is called.
auto fn = std::bind(&NAME::f, n); 

// assignment operator makes a copy of members of fn to members of std::function. 
// This is also a copy constructor call of NAME
std::function<void ()> callable = fn; 

Note: Instead of bind, you can also use lambda's

std::function<void ()> callable([&n]{n.f()});
like image 33
Pepijn Kramer Avatar answered Apr 30 '23 11:04

Pepijn Kramer