Today the post on isocpp.org called Learn How to Capture By Move caught my attention. I found the post informative and thought provoking, and you should go and read it before reading the rest of this post.
The problem is how do we lambda capture a large object we want to avoid copying.
The motivating example is below
The problem is how do we lambda capture a large object we want to avoid copying.
The motivating example is below
function<void()> CreateLambda()
{
vector<HugeObject> hugeObj;
// ...preparation of hugeObj...
auto toReturn = [hugeObj] { ...operate on hugeObj... };
return toReturn;
}
The solution proposed is a template class move_on_copy that is used like this
auto moved = make_move_on_copy(move(hugeObj));
auto toExec = [moved] { ...operate on moved.value... };
However, there are problems with this approach, mainly in safety. The move_on_copy acts as auto_ptr and silently performs moves instead of copies (by design).
I present here a different take on the problem which accomplishes much of the above safely, however, with a little more verbosity in exchange for more clarity and safety.
First let me tell you how you would use the final product
HugeObject hugeObj;
// ...preparation of hugeObj...
auto f = create_move_lambda(std::move(hugeObj),[](moved_value<HugeObject> hugeObj){
// manipulate huge object
// In this example just output it
std::cout << hugeObj.value() << std::endl;
});
The point of interest is the create_move_lambda
The first argument is r-value reference of the object we want to move generated with std::move
The second argument is the lambda
Instead of having the moved object in the capture list, we take an extra argument of moved_value which looks like this
template<class T>
using moved_value = std::reference_wrapper<T>;
You can access the moved object using moved_value.get()
Currently, you can have any number or parameters or any return type for your lambda, but only 1 move capture. That restriction, I am sure could eventually be removed.
So how does this work? Instead of attempting to change the capture type or wrap the capture type, we instead create a function object which wraps the lambda, stores the moved object, and when called with a set of arguments, forwards to the lambda with the moved_value as the first parameter. Below is the implementation of move_lambda and create_move_lambda
template<class T,class F>
struct move_lambda{
private:
T val;
F f_;
public:
move_lambda(T&& v, F f):val(std::move(v)),f_(f){};
move_lambda(move_lambda&& other) = default;
move_lambda& operator=(move_lambda&& other) = default;
template<class... Args>
auto operator()(Args&& ...args) -> decltype(this->f_(moved_value<T>(this->val),std::forward<Args>(args)...))
{
moved_value<T> mv(val);
return f_(mv,std::forward<Args>(args)...);
}
move_lambda() = delete;
move_lambda(const move_lambda&) = delete;
move_lambda& operator=(const move_lambda&) = delete;
};
template<class T,class F>
move_lambda<T,F>create_move_lambda(T&& t, F f){
return move_lambda<T,F>(std::move(t),f);
}
So now we have move_lambda returned from create_move_lambda that can be used just like a lambda with move capture. In addition, copy construction and assignment are disabled so you cannot inadvertently copy the lambda. However, move construction and move assignment are enabled so you can move the lambda. Further examples are below
// A movable only type, not copyable
TestMove m;
m.k = 5;
// A movable only type, not copyable
TestMove m2;
m2.k = 6;
// Create a lambda that takes 2 parameters and returns int
auto lambda = create_move_lambda(std::move(m),[](moved_value<TestMove> m,int i,int)->int{
std::cout << m.get().k << " " << i << std::endl;return 7;
});
// Create a lambda that takes 0 parameters and returns void
auto lambda2 = create_move_lambda(std::move(m2),[](moved_value<TestMove> m){
std::cout << m.get().k << std::endl;
});
std::cout << lambda(1,2) << std::endl;
lambda2();
// Compiler error if you try to copy
//auto lambda4 = lambda;
// Able to move
auto lambda3 = std::move(lambda2);
lambda3();
However, there is still one more problem left in using move_lambda. You cannot store move_lambda in a std::function because move_lambda does not have a copy constructor. So how do we write the original function we wanted. Well we write a movable_function which is presented below
// Unfortunately, std::function does not seem to support move-only callables
// See § 20.8.11.2.1 point 7 where it requires F be CopyConstructible
// From draft at http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2011/n3242.pdf
// Here is our movable replacement for std::function
template< class ReturnType, class... ParamTypes>
struct movable_function_base{
virtual ReturnType callFunc(ParamTypes&&... p) = 0;
};
template<class F, class ReturnType, class... ParamTypes>
struct movable_function_imp:public movable_function_base<ReturnType,ParamTypes...>{
F f_;
virtual ReturnType callFunc(ParamTypes&&... p){
return f_(std::forward<ParamTypes>(p)...);
}
explicit movable_function_imp(F&& f):f_(std::move(f)){};
movable_function_imp() = delete;
movable_function_imp(const movable_function_imp&) = delete;
movable_function_imp& operator=(const movable_function_imp&) = delete;
};
template<class FuncType>
struct movable_function{};
template<class ReturnType, class... ParamTypes>
struct movable_function<ReturnType(ParamTypes...)>{
std::unique_ptr<movable_function_base<ReturnType,ParamTypes...>> ptr_;
template<class F>
explicit movable_function(F&& f):ptr_(new movable_function_imp<F,ReturnType,ParamTypes...>(std::move(f))){}
movable_function(movable_function&& other) = default;
movable_function& operator=(movable_function&& other) = default;
template<class... Args>
auto operator()(Args&& ...args) -> ReturnType
{
return ptr_->callFunc(std::forward<Args>(args)...);
}
movable_function() = delete;
movable_function(const movable_function&) = delete;
movable_function& operator=(const movable_function&) = delete;
};
Based on the above we can write our CreateLambda() asmovable_function<void()> CreateLambda()
{
// Pretend our TestMove is a HugeObject that we do not want to copy
typedef TestMove HugeObject;
// Manipulate our "HugeObject"
HugeObject hugeObj;
hugeObj.k = 9;
auto f = create_move_lambda(std::move(hugeObj),[](moved_value<HugeObject> hugeObj){// manipulate huge object
std::cout << hugeObj.get().k << std::endl;
});
movable_function<void()> toReturn(std::move(f));
return toReturn;
}
And use it like this
// Moved out of function
auto lambda4 = CreateLambda();
lambda4();
Alternatives and Extensions
A simple alternative would be instead of using moved_value as the first parameter, take a reference. This would make the lambda look like this.
// You can take a reference instead of a moved_value if you want
auto lambda5 = create_move_lambda(std::move(m3),[](TestMove& m){
std::cout << m.k << std::endl;
});
This actually works due to moved_value being a template alias for reference_type. This is a little bit shorter than the previous code, but you cannot tell what are your real lambda parameters and what are the parameters used to simulate move capture.
An extension to this code would be allowing for multiple captured variables. Currently, the code only allows for 1 move captured variable.
Thanks for taking the time to read this.
You can find a compilable example at https://gist.github.com/4208898
The code above requires a fairly compliant C++11 compiler and has been tested with GCC 4.7.2 (Windows nuwen.net distribution).
An older version of the code that compiles with ideone and with VC++ 2012 November CTP is at http://ideone.com/OXYVyp
Please leave comments and let me know what you think.
- John Bandela
Modified 12/4/2012
Many thanks to Eric Niebler for his comments on the previous version of the code for this article.
Cool idea. Would you be interested in turning this into a follow-up post on isocpp.org? Please contact me at eric -dot- niebler -at- gmail -dot- com.
ReplyDeleteCool! keep it up!
ReplyDeleteThanks
DeleteCool indeed!
ReplyDeleteA thought for future C++ versions: Why not allow initialization of new variables in the capture list, e.g.:
function CreateLambda()
{
vector hugeObj;
// ...preparation of hugeObj...
auto toReturn =
[vector moved(move(hugeObject))]
{ ...operate on moved... };
return toReturn;
}
This would make lambdas an even neater tool.
BTW, anyone else wanting to allow the following in future C++:
* template lambdas (e.g. template [](T x){... work with x ...}), and
* using auto in argument list to denote an anonymous template class argument (e.g. int f(auto x){} <=> template int f(T x){}
* relaxed rules for type deduction of return type, and allowing it for all functions. How neat is the current syntax of "-> decltype(some long expression, basically being the function body again)"?
Edit:
Deleteint f(auto x){} <=> template int f(T x){}
should be
int f(auto x){...} <=> template int f(T x){...}
Hmmm, my leftAngleBracket class T rightAngleBracket were removed in the post. You'll have to use some imagination...
DeleteIn regards to your lambda changes, take a look at Dave Abrahams Post from cppnext
Deletehttp://cpp-next.com/archive/2011/11/having-it-all-pythy-syntax/
This is cool, but I wonder if it's necessary. Couldn't we just repurpose std::bind() ?
ReplyDeletefunction CreateLambda()
{
vector hugeObj;
// ...preparation of hugeObj...
auto toReturn = std::bind(
[](vector & hugeObj)
{ ...operate on hugeObj... },
std::move(hugeObj));
return toReturn;
}
This is a great suggestion, and it's what I will use in the future when I need this functionality. Thanks!
DeleteI believe doing this will create an extra copy of hugeObj on return.
ReplyDeleteI would expect that the return is implicitly an rvalue, and std::function has a perfect forwarding constructor (doesn't it?), so ultimately std::bind's (and std::vector's) move constructor will get called to place it into std::function.
DeleteThat's correct. You can test this by using the above method to capture and return a move-only object. It works, and the callable object returned from std::bind is suitably move-only itself.
DeleteI really enjoyed the article, this is very well-written and helpful. Although, I have been searching for long about the topic but finally I got the right article. The information you have shared with is really incredible. Keep it up
ReplyDeletePlease also send me a link for an informative blog on web design services sydney
Great post . It takes me almost half an hour to read the whole post. Definitely this one of the informative and useful post to me. Thanks for the share and plz visit my site Programming 21st and century provide offshore outsourcing service, freelance service, personal assistant,web development, customer service, marketing services.
ReplyDelete