Introduction
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:
function<void()> CreateLambda()
{
vector<HugeObject> 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;
auto f = create_move_lambda(std::move(hugeObj),[](moved_value<HugeObject> hugeObj){
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%lt;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:
TestMove m;
m.k = 5;
TestMove m2;
m2.k = 6;
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;
});
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();
auto lambda3 = std::move(lambda2);
lambda3();
However, there is still one more problem left in using move_lambda
. You cannot store
<code>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:
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()
as:
movable_function<void()> CreateLambda()
{
typedef TestMove HugeObject;
HugeObject hugeObj;
hugeObj.k = 9;
auto f = create_move_lambda(std::move(hugeObj),[]
(moved_value<HugeObject> hugeObj){ std::cout << hugeObj.get().k << std::endl;
});
movable_function<void()> toReturn(std::move(f));
return toReturn;
}
And use it like this:
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:
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.