I still don't have a great grasp of perfect forward and move references. I'm trying to understand the differences in regards to passing lambda expressions around. My assumption was that I'd use std::function<..> or auto to accept lambda function types, but then looking at the Folly source code I see that they're using function templates.
I wrote a small test program to try and understand the differences, with both lvalue and rvalues, but I can't see any. Are there any differences between the SetLambda*() variants below?
As far as I can tell, the only one which doesn't work is SetLambda5() when given an lvalue. For reference, I'm using a GCC version supporting C++14.
struct MyClass {
template<typename Lambda>
void SetLambda(Lambda&& lambda) { mLambda = std::forward<Lambda>(lambda); }
template<typename Lambda>
void SetLambda2(Lambda&& lambda) { mLambda = lambda; }
template<typename Lambda>
void SetLambda3(Lambda lambda) { mLambda = lambda; }
void SetLambda4(auto lambda) { mLambda = lambda; }
//void SetLambda5(auto& lambda) { mLambda = lambda; }
void SetLambda6(auto&& lambda) { mLambda = lambda; }
void SetLambda7(std::function<void()> lambda) { mLambda = lambda; }
void Run() { mLambda(); }
std::function<void()> mLambda;
};
int main() {
auto lambda = []() { std::cout << "test0\n"; };
MyClass myClass;
myClass.SetLambda([]() { std::cout << "test1\n"; });
myClass.Run();
myClass.SetLambda(lambda);
myClass.Run();
myClass.SetLambda2([]() { std::cout << "test2\n"; });
myClass.Run();
myClass.SetLambda2(lambda);
myClass.Run();
myClass.SetLambda3([]() { std::cout << "test3\n"; });
myClass.Run();
myClass.SetLambda3(lambda);
myClass.Run();
myClass.SetLambda4([]() { std::cout << "test4\n"; });
myClass.Run();
myClass.SetLambda4(lambda);
myClass.Run();
//myClass.SetLambda5([]() { std::cout << "test5\n"; });
//myClass.Run();
//myClass.SetLambda5(lambda);
//myClass.Run();
myClass.SetLambda6([]() { std::cout << "test6\n"; });
myClass.Run();
myClass.SetLambda6(lambda);
myClass.Run();
myClass.SetLambda7([]() { std::cout << "test7\n"; });
myClass.Run();
myClass.SetLambda7(lambda);
myClass.Run();
return 0;
}
And, for reference, the output:
test1
test0
test2
test0
test3
test0
test4
test0
test6
test0
test7
test0
When accepting an unknown functor that you're going to call directly without storing it, the ideal, value category preserving way to do so is:
template <typename Func>
void DoAThing(Func&& func) {
std::forward<Func>(func)(parameters);
}
When you want to store the functor in a std::function object to call later, just accept a std::function and let the implicit conversion do most of the work for you:
void StoreAFunctor(std::function<void()> func) {
myFunctor = std::move(func);
}
Before diving into a deeper explanation, the first thing to mention is that the point of using move semantics and perfect forwarding is to avoid doing expensive copy operations. We want to move ownership of resources around when possible instead of unnecessarily copying them. If your objects don't own any movable resources (as is the case of a lambda with no captures), then none of this matters. Just pass the object by reference-to-const and copy it as needed. If your objects do own some movable resources, then things get hairy.
Before talking about lambdas and std::function, I'm going to take a step back and look at how things work with this simple type that shows what's going on:
struct ShowMe {
ShowMe() { }
ShowMe(const ShowMe&) { std::cout << "ShowMe copy constructed\n"; }
ShowMe(ShowMe&&) { std::cout << "ShowMe move constructed\n"; }
ShowMe& operator=(const ShowMe&) { std::cout << "ShowMe copy assigned\n"; return *this; }
ShowMe& operator=(ShowMe&&) { std::cout << "ShowMe move assigned\n"; return *this; }
};
I'm also going to use this simple type as a stand-in for std::function:
struct ShowMeHolder {
ShowMeHolder() { }
ShowMeHolder(const ShowMe& object) : mObject{object} { }
ShowMeHolder(ShowMe&& object) : mObject{std::move(object)} { }
ShowMeHolder& operator=(const ShowMe& object) { mObject = object; return *this; }
ShowMeHolder& operator=(ShowMe&& object) { mObject = std::move(object); return *this; }
ShowMe mObject;
};
Using that type, here's an example that reproduces all of your test cases (plus a few variants):
struct MyClass {
template<typename Object>
void SetObject(Object&& object) { mObject = std::forward<Object>(object); }
template<typename Object>
void SetObject2(Object&& object) { mObject = object; }
template<typename Object>
void SetObject3(Object object) { mObject = object; }
template <typename Object>
void SetObject3Variant(Object object) { mObject = std::move(object); }
void SetObject4(auto object) { mObject = object; }
void SetObject4Variant(auto object) { mObject = std::move(object); }
void SetObject5(auto& object) { mObject = object; }
void SetObject6(auto&& object) { mObject = object; }
void SetObject6Variant(auto&& object) { mObject = std::forward<decltype(object)>(object); }
void SetObject7(ShowMeHolder object) { mObject = object; }
void SetObject7Variant(ShowMeHolder object) { mObject = std::move(object); }
ShowMeHolder mObject;
};
int main() {
MyClass myClass;
ShowMe object;
std::cout << "SetObject move\n";
myClass.SetObject(std::move(object));
std::cout << "SetObject copy\n";
myClass.SetObject(object);
std::cout << "SetObject2 move\n";
myClass.SetObject2(std::move(object));
std::cout << "SetObject2 copy\n";
myClass.SetObject2(object);
std::cout << "SetObject3 move\n";
myClass.SetObject3(std::move(object));
std::cout << "SetObject3 copy\n";
myClass.SetObject3(object);
std::cout << "SetObject3Variant move\n";
myClass.SetObject3Variant(std::move(object));
std::cout << "SetObject3Variant copy\n";
myClass.SetObject3Variant(object);
std::cout << "SetObject4 move\n";
myClass.SetObject4(std::move(object));
std::cout << "SetObject4 copy\n";
myClass.SetObject4(object);
std::cout << "SetObject4Variant move\n";
myClass.SetObject4Variant(std::move(object));
std::cout << "SetObject4Variant copy\n";
myClass.SetObject4Variant(object);
//std::cout << "SetObject5 move\n";
//myClass.SetObject5(std::move(object));
std::cout << "SetObject5 copy\n";
myClass.SetObject5(object);
std::cout << "SetObject6 move\n";
myClass.SetObject6(std::move(object));
std::cout << "SetObject6 copy\n";
myClass.SetObject6(object);
std::cout << "SetObject6Variant move\n";
myClass.SetObject6Variant(std::move(object));
std::cout << "SetObject6Variant copy\n";
myClass.SetObject6Variant(object);
std::cout << "SetObject7 move\n";
myClass.SetObject7(std::move(object));
std::cout << "SetObject7 copy\n";
myClass.SetObject7(object);
std::cout << "SetObject7Variant move\n";
myClass.SetObject7Variant(std::move(object));
std::cout << "SetObject7Variant copy\n";
myClass.SetObject7Variant(object);
}
This gives the following output:
SetObject move
ShowMe move assigned
SetObject copy
ShowMe copy assigned
SetObject2 move
ShowMe copy assigned
SetObject2 copy
ShowMe copy assigned
SetObject3 move
ShowMe move constructed
ShowMe copy assigned
SetObject3 copy
ShowMe copy constructed
ShowMe copy assigned
SetObject3Variant move
ShowMe move constructed
ShowMe move assigned
SetObject3Variant copy
ShowMe copy constructed
ShowMe move assigned
SetObject4 move
ShowMe move constructed
ShowMe copy assigned
SetObject4 copy
ShowMe copy constructed
ShowMe copy assigned
SetObject4Variant move
ShowMe move constructed
ShowMe move assigned
SetObject4Variant copy
ShowMe copy constructed
ShowMe move assigned
SetObject5 copy
ShowMe copy assigned
SetObject6 move
ShowMe copy assigned
SetObject6 copy
ShowMe copy assigned
SetObject6Variant move
ShowMe move assigned
SetObject6Variant copy
ShowMe copy assigned
SetObject7 move
ShowMe move constructed
ShowMe copy assigned
SetObject7 copy
ShowMe copy constructed
ShowMe copy assigned
SetObject7Variant move
ShowMe move constructed
ShowMe move assigned
SetObject7Variant copy
ShowMe copy constructed
ShowMe move assigned
Live Demo
I'll go through each and explain why they behave the way they do:
SetObject: This function accepts a forwarding reference. When combined with std::forward these preserve the value category of the object passed to them. That means that when we call SetObject(object) object gets copy-assigned from and when we call SetObject(std::move(object)) object gets move-assigned from.SetObject2: This function accepts a forwarding reference, but since you didn't use std::forward to preserve the value category of the parameter it is always an lvalue, and therefore copy-assigned from.SetObject3: This function accepts its parameter by value. The parameter object is either copy or move constructed based on the value category of the object passed to the function, but then the parameter object is always copy-assigned from since it is an lvalue.SetObject3Variant: This function, like SetObject3 accepts its parameter by value and the parameter object is either copy or move constructed based on the value category of the object passed to the function. We then use std::move to cast the parameter object to an rvalue causing it to be move-assigned from instead of copy-assigned.SetObject4: This function works exactly like SetObject3. The auto parameter is just syntactic sugar for a template.SetObject4Variant: This function works exactly like SetObject3VariantSetObject5: This function accepts its parameter by lvalue-reference-to-non-const. Those can only bind to lvalues, so you can't pass it an rvalue at all. Its parameter gets copy-assigned from since it's an lvalue.SetObject6: This works exactly like SetObject2. Again, auto parameters are just syntactic sugar for templates.SetObject6Variant: This works exactly like SetObject except that the std::forward syntax is a bit wonky since you don't have an explicit template type parameter to refer to.SetObject7: This function accepts a ShowMeHolder by value. That object will be constructed using either the const ShowMe& or ShowMe&& constructor based on the value category of the ShowMe object passed to it. The function then copy-assigns the ShowMeHolder parameter object to the class member since the parameter is an lvalue.SetObject7Variant: This function works similarly to SetObject7, but the parameter object is move-assigned from since it was cast to an rvalue using std::move.Bringing it back to lambdas, everything works exactly the same. Just replace ShowMe with some lambda type and ShowMeHolder with std::function. There's nothing special about either of those types. Lambdas are just objects with an overloaded operator(), and std::function is just an object that holds some other function-like object (using a bunch of tricks to be able to store any type of function-like object).
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With