Lambda 移动捕获的另一种替代方案





5.00/5 (1投票)
这篇文章讨论了lambda移动捕获的另一种替代方案。
引言
今天,isocpp.org上的一篇文章,名为学习如何通过移动捕获吸引了我的注意。我发现这篇文章信息量很大,发人深省,你应该在阅读本文的其余部分之前先阅读它。
问题是如何捕获一个大型对象,而我们又想避免复制它。
下面的例子说明了这个问题
function<void()> CreateLambda()
{
vector<HugeObject> hugeObj;
// ...preparation of hugeObj...
auto toReturn = [hugeObj] { ...operate on hugeObj... };
return toReturn;
}
提出的解决方案是一个模板类move_on_copy
,使用方法如下
auto moved = make_move_on_copy(move(hugeObj));
auto toExec = [moved] { ...operate on moved.value... };
然而,这种方法存在一些问题,主要是在安全性方面。move_on_copy
的作用类似于auto_ptr
,它会默默地执行移动而不是复制(这是其设计意图)。
我在这里提出了一种不同的解决方法,它可以安全地完成上述大部分工作,但是,需要稍微多一些冗余代码,以换取更高的清晰度和安全性。
首先,让我告诉你如何使用最终产品
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;
});
值得关注的是create_move_lambda
。第一个参数是我们想要移动的对象的右值引用,使用std::move
生成。
第二个参数是lambda表达式。
我们没有将移动的对象放在捕获列表中,而是添加了一个额外的参数moved_value
,它看起来像这样
template<class T>
using moved_value = std::reference_wrapper<T>;
你可以使用moved_value.get()
访问移动的对象。
目前,你的lambda表达式可以有任意数量的参数或任意返回类型,但只能进行一次移动捕获。我相信这个限制最终可以消除。
那么这是如何工作的呢?我们不是试图更改捕获类型或包装捕获类型,而是创建一个函数对象来包装lambda表达式,存储移动的对象,并在用一组参数调用时,将moved_value
作为第一个参数转发给lambda表达式。以下是move_lambda
和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);
}
现在,我们有create_move_lambda
返回的move_lambda
,它可以像带有移动捕获的lambda表达式一样使用。此外,复制构造函数和赋值运算符被禁用,因此你无法意外地复制lambda表达式。但是,移动构造函数和移动赋值运算符是启用的,因此你可以移动lambda表达式。下面是更多示例
// 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();
然而,使用move_lambda
仍然存在一个问题。你不能将
存储在move_lambda
std::function
中,因为move_lambda
没有复制构造函数。那么我们如何编写我们想要的原始函数呢?我们可以编写一个movable_function
,如下所示
// 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;
};
基于以上内容,我们可以将我们的CreateLambda()
编写为
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;
}
并像这样使用它
// Moved out of function
auto lambda4 = CreateLambda();
lambda4();
替代方案和扩展
一个简单的替代方案是,不使用moved_value
作为第一个参数,而是使用引用。这将使lambda表达式看起来像这样
// 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;
});
这实际上有效,因为moved_value
是reference_type
的模板别名。这比之前的代码稍微短一些,但是你无法分辨哪些是你的实际lambda参数,哪些是用来模拟移动捕获的参数。
此代码的一个扩展是允许多个被捕获的变量。目前,代码只允许一个移动捕获变量。
感谢您抽出时间阅读本文。
你可以在https://gist.github.com/4208898找到一个可编译的示例。
上述代码需要一个相当兼容的C++11编译器,并且已在GCC 4.7.2(Windows nuwen.net发行版)上进行了测试。
可以在http://ideone.com/OXYVyp找到一个与ideone和VC++ 2012 November CTP兼容的旧版本代码。
请留下评论,让我知道你的想法。