Another Alternative to Lambda Move Capture





5.00/5 (1 vote)
This post discusses another alternative to lambda move capture.
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;
// ...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%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:
// 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
in a move_lambda
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()
as:
movable_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.