What exactly std::function
constructor does when stored in a container ?
In this test codes:
struct A {
int sn;
A() = delete;
A(int v) : sn(v) { cout << "A::init(" << sn << ')' << endl; }
A(const A& a) : sn(a.sn+1) { cout << "A::copy(" << sn << ')' << endl; }
A(A&& a) : sn(a.sn+1) { cout << "A::move(" << sn << ')' << endl; }
~A() { cout << "A::delete(" << sn << ')' << endl; }
};
void func(int a, A &b) {
cout << "func2:" << a << ',' << b.sn << endl;
}
int main(int argc, char *argv[]) {
std::vector<std::function<void(void)>> fv;
A a(1);
cout << "call bind()" << endl;
fv.emplace_back(std::bind(func,1,a));
fv.front()();
cout << "end of local scope" << endl;
}
I declare the move and copy constructors of class A
to accumulate a sequence number, so that I can track it as the Nth created instance. The result is:
A::init(1)
call bind()
A::copy(2)
A::move(3)
A::move(4)
A::delete(3)
A::delete(2)
func2:1,4
end of local scope
A::delete(1)
A::delete(4)
First an instance a(1)
is created in main()
local scope, then it's copied as a new instance a(2)
when calling std::bind()
, then moved again into std::function
dynamically allocated memory (or its internal memory footage?) to hold a copy of std::bind()
instance, so we have the 3rd instances of A
held by std::function
instance.
So far it's understandable, but why is there another move construction ? And why the 3rd instance is destructed before the 2nd instance, which is supposed to be cleared when std::bind()
returns ?
If I rewrite the main()
function as:
int main(int argc, char *argv[]) {
A a(1);
cout << "call bind()" << endl;
std::function<void(void)> f(std::bind(func,1,a));
f();
cout << "end of local scope" << endl;
}
then the result would be:
A::init(1)
call bind()
A::copy(2)
A::move(3)
A::delete(2)
func2:1,3
end of local scope
A::delete(3)
A::delete(1)
There is no second move construction and everything looks reasonable.
What exactly happened when calling emplace_back()
to create std::function
instance in this case ?
First an instance
a(1)
is created inmain()
local scope, then it's copied as a new instancea(2)
when callingstd::bind()
, then moved again intostd::function
dynamically allocated memory
You're missing a step: std::function
's constructor takes by-value
template<typename F>
std::function<void(void)>::function(F f);
emplace_back
is forwarding the std::bind
-wrapper containing a(2)
as an rvalue, yes, but this constructor only accepts by value, not by rvalue reference, so the a(2)
gets moved and becomes a(3)
just to initialize the parameter of the constructor. Then the constructor moves that value out into dynamic storage.
int main() {
A a(1); // A::init(1)
// this is the temporary that gets materialized in main's scope for the call to emplace_back
auto wrapper = std::bind(func, 1, a); // A::copy(2);
// emplace_back receives (a reference to) the temporary as an rvalue, and so it passes std::function's constructor a reference to the same object, also as an rvalue
// but std::function's constructor only takes values...
std::function<void(void)> f(std::move(wrapper));
// initializing the parameter of constructor (in main/emplace_back's context): A::move(3)
// constructor then initializes dynamically allocated object: A::move(4)
// constructor destroys parameter object: A::delete(3)
// f gets destroyed, takes dynamically allocated object with it: A::delete(4)
// wrapper (the temporary) gets destroyed (originally at the ; after emplace_back): A::delete(2)
// a gets destroyed: A::delete(1);
}
// Note: the reason A::delete(4) comes before A::delete(1) in my version but not yours is that your A(4) lives inside fv, which is created at the top of main and thus is the last thing destroyed, but mine lives inside f, which is created at the end and is the first thing destroyed
The difference in your simplification is that you're not materializing the result of std::bind
as a temporary. Basically, emplace_back
requires that the arguments already be fully constructed (as does any function call). So you have to execute the std::bind
(and the copy inside it) just to call emplace_back
, and then emplace_back
moves that object to the constructor parameter. But, when you pass the std::bind
prvalue directly to the std::function
constructor, the constructor parameter object is directly constructed by setting std::bind
's result object to the parameter object during its execution.
Another way to say it: std::function
's constructor must be called with a newly created object. When using emplace_back
, the intervening function "forgets" the std::bind
-call was a newly constructed object, so a new object is constructed by doing an "extraneous" move. When you remove emplace_back
, then the fact that the std::bind
object is newly constructed is not forgotten and the move is elided.
Another another way to say it: "perfect" forwarding as claimed by emplace_back
is actually not perfect; such a thing isn't possible (the user code must be modified). "Perfect" forwarding forwards lvalues as lvalues, but collapses both xvalues and prvalues as xvalues (by requiring prvalues be materialized before the function is even called). Calling std::function
's constructor with an xvalue causes two moves, but calling with a prvalue causes one. Your second version passes a prvalue to the constructor, but the original passes an xvalue, so they have different behavior.
You can get the desired result with a wrapper functor:
template<typename F>
struct initializer {
F f;
operator decltype(f())() && {
return std::move(f)();
}
};
int main(int argc, char *argv[]) {
std::vector<std::function<void(void)>> fv;
A a(1);
cout << "call bind()" << endl;
fv.emplace_back(
initializer{[&]() -> std::function<void(void)> {
return std::bind(func, 1, a);
}});
fv.front()();
cout << "end of local scope" << endl;
}
This puts the call to std::bind
directly under the call to std::function
's constructor (which is in the return
), so the prvalue directly initializes the constructor parameter with no intervening move. The std::function
is also constructed directly in the "empty" space in the vector, with no further moving (that's what emplace_back
does, after all).