65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIconstarIcon

5.00/5 (1投票)

2012年12月31日

CPOL

3分钟阅读

viewsIcon

9343

这篇文章讨论了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_lambdacreate_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_valuereference_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兼容的旧版本代码。

请留下评论,让我知道你的想法。

© . All rights reserved.