C++17 关键新特性介绍及代码讲解 (7) --- lambda capture of ‘*this‘ by value

C++17 关键新特性介绍及代码讲解 (7) --- lambda capture of ‘*this‘ by value,第1张

C++17 关键新特性介绍及代码讲解 (7) — lambda capture of ‘*this’ by value

一句话概括:

在定义 lambda 的 captures 时,可以通过 [*this]这种形式,将当前 this 所指向的 object,
以 value-copy 的方式拷贝下来,并在 lambda 的表达式中使用拷贝得到的 object 成员变量值。

完整 demo 代码参见 gitee.com 上的公开代码仓库 :
[lambda_capture_of_this_by_value]

对于在一个 non-static 成员函数中声明的 lambda 表达式,如果需要访问当前 object 的成员变量,在 C++17 之前,
我们需要 capture 代表当前对象的 ‘this’ 指针,再通过 ‘this’ 去访问当前 object 的成员变量:有三种 capture 的方式,
[=][&][this]; 但这三种方式,都只能让 lambda 表达式中访问当前 object 的成员变量方式,全都为 by reference,
做不到真正的 by value copy,我们来看一下例子代码:

/* to capture (*this) */
struct S {
    S(int i) : m_i(i) {};
    int m_i;
    // all of get_a, get_b and get_c, capture (*this) by reference.
    auto get_a() {
        // implicit capture of (*this) by reference.
        auto a = [&] { return m_i; };  // transformed to (*this).m_i
        return a;
    }
    auto get_b() {
        // implicit capture of (*this) by reference, even implicit capture the used automatic variables by copy, i.e. '='.
        auto b = [=] { return m_i; }; // transformed to (*this).m_i
        return b;
    }    
    auto get_c() {
        // explicit capture of (this) by copy, then capture of (*this) by reference.
        auto c = [this] { return m_i; }; // transformed to (*this).m_i
        return c;
    }
}

int main(){
    // test capture of (*this).
    auto s = S(10);
    // capture of (*this) by reference in a, b, c.
    auto a = s.get_a();
    auto b = s.get_b();
    auto c = s.get_c();

    s.m_i = 100;
    // s.m_i value changed to 100, so do the return value of a(), b(), and c().
    assert(a() == 100 and b() == 100 and c() == 100);

    s.m_i = 200;
    // s.m_i value changed to 200, so do the return value of a(), b(), and c().
    assert(a() == 200 and b() == 200 and c() == 200);
    return 0;
}

可以看到,在 struct S 中的 non-static 成员函数 get_a(), get_b(), get_c() 中定义的 lambda,访问当前 object ( 代码中即 s )
的成员变量 m_i 的方式,都为 by reference; 当 s.m_i 的值改变的时候,lambda 表达式回的值,也跟着改变;

lambda 作为异步和并发编程的基础 *** 作,如果只能对当前 object 进行 by reference 的 capture 的话,容易造成我们编程时的各种“陷阱”,比如下面的例子:


class Worker {
public:
    Worker(int v) : m_i(v){};
    auto get_job_capture_this_by_ref(){
        // implicit capture of (*this) by reference.
        return std::async(std::launch::async, [=]{ return  m_i * m_i;});
    }
private:
    int m_i;
};

auto dispatch_job_will_cause_error(int v){
    auto worker = Worker(v);
    return worker.get_job_capture_this_by_ref();
    // NOTE: the lambda associated with the returned std::future
    // has an implicit capture of 'this' pointer which is invalid now.
}

int main(){
    auto job1 = dispatch_job_will_cause_error(100);
    std::this_thread::yield();
    auto ret1 = job1.get();
    assert(ret1 != 100 * 100 );
    std::cout << "value of ret1: " << ret1 << std::endl;
}

从代码运行打印来看,ret1 已经是一个异常值:

value of ret1: 1073676289

分析一下:在 dispatch_job_will_cause_error(int v)函数中,我们实例化了一个worker对象,然后通过调用
worker.get_job_capture_this_by_ref()得到了一个 std::future 对象,
该 future 对象关联了一个在 Worker::get_job_capture_this_by_ref()中定义的 lambda:

[=]{ return  m_i * m_i;}

该 lambda 使用 [=]的方式,实际上对上述 worker 对象的 capture by reference;当代码执行从
dispatch_job_will_cause_error(int v)函数中退出时,worker 被析构,但是在 int main()函数中,当代码执行到
auto ret1 = job1.get();时,上述 lambda 会直接对 worker 对象中的成员变量 m_i 进行访问,但是因为 worker 对象已经被析构,所以
读取到的是一个内存中的随机值,最后得到 1073676289这个异常值。

所以,为了让 lambda 更好的适应异步编程的情况,减少类似的编程错误,C++17 增加了一种新的 capture 的方式:

*this: 通过 by value copy 的方式,capture 当前的 object, 比如,下面这个 lambda 表达式:

[*this](){ return m_i;};

会被转换成等同于下面这种形式:

[ _tmp=*this ](){ return _tmp.m_i;}

即,会生成一个临时对象 _tmp, 该临时对象拷贝自 *this,然后在 lambda 表达式中,所有对当前对象成员的访问,都转换成对临时对象 _tmp 中
对等成员的访问。

来看一个简单的例子:


struct S {
    S(int i):m_i(i) {};
    // since C++17, we can capture (*this) by value copy, explicitly.
    auto get_d(){        
        auto d = [*this](){ return m_i;}; // transformed to [ _tmp=*this ](){ return _tmp.m_i;}
        return d;
    }
};

int main(){
    // test capture of (*this).
    auto s = S(88);
    // capture of (*this) by value copy.
    auto d = s.get_d();
    
    s.m_i = 400;
    assert(d() == 88);
}

可以看到,在 struct S 中的 non-static 成员函数 get_d() 中定义的 lambda,访问当前 object ( 代码中即 s ) 的成员变量 m_i 的方式,
即为 by value copy 的方式,当 s.m_i 的值改变的时候,lambda 表达式返回的值,不会跟着改变,保持为 lambda 表达式被解析时的 88;

再回到上面异步编程的例子,通过使用 *this这种 capture by value copy 的方式,我们可以得到正确的结果:


class Worker {
public:
    Worker(int v) : m_i(v){};
    auto get_job_capture_this_by_value_copy(){
        //since C++17: capture of (*this) by value copy.
        return std::async(std::launch::async, [*this]{ return  m_i * m_i;});
    }  
private:
    int m_i;
};

auto dispatch_job_correctly(int v){
    auto worker = Worker(v);
    // the lambda associated with the returned std::future
    // has a copy of 'worker' object.
    return worker.get_job_capture_this_by_value_copy();
}

int main(){
    auto job2 = dispatch_job_correctly(200);
    std::this_thread::yield();
    auto ret2 = job2.get();
    assert(ret2 == 200 * 200 );
    std::cout << "value of ret2: " << ret2 << std::endl;
}

运行上述代码打印如下:

value of ret2: 40000

补充说明 : 其实从 C++14 开始,我们如果使用 by-copy capture with an initializer 的方式,也是可以实现 capture of (*this) by value copy 的效果:

class Worker {   
    int m_i;
    auto get_job_capture_this_by_value_copy(){
        // since C++14: by-copy capture of (*this) with initializer
        return std::async(std::launch::async, [=, tmp_obj=*this]{ 
            // NOTE: every reference to a member of Worker, must be qualified with 'tmp_obj.'
            // Any mistaken omissions will silently fail as reference via 'this->',that is,still
            // capture by reference.
            return tmp_obj.m_i * tmp_obj.m_i;
        ;});
    }    
};

[=, tmp_obj=*this]这种形式,容易引入因疏忽造成的 bug: 在 lambda 表达式中,对每一个 Worker 成员变量的引用,
都要显示的加上 ‘tmp_obj.’ 前缀,如果忽略了该前缀,则实际的效果又成为了 ‘this->’ 这种引用的方式,这对代码的维护带来隐患,
特别是 lambda 的实现比较复杂的情况下。

欢迎分享,转载请注明来源:内存溢出

原文地址:https://www.54852.com/langs/1498666.html

(0)
打赏 微信扫一扫微信扫一扫 支付宝扫一扫支付宝扫一扫
上一篇 2022-06-25
下一篇2022-06-25

发表评论

登录后才能评论

评论列表(0条)

    保存