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.