踩坑记录【二】【C++】引用捕获常量的生命周期

38 阅读3分钟

OC代码写多了,在闭包里用引用捕获就随便了起来,类似的写法,在 cpp 是有问题的

被 C++ lambda捕获的常量的生命周期并不会发生改变,并不会因为捕获有额外的生命周期,该释放的时候还是会释放,如果闭包超出了常量原有的生命周期,调用常量时就会出问题。

问题代码

void test() {
  int a = 20;
  runAsyncFunc(base::BindLambda([&] {
    a = a + 10;	
    print("%d", a); // 乱码; 引用捕获
  });
}

问题分析

如果我们需要再闭包里用到 int a,在 oc 里我们会这么写:

nslog 的时候,a 依旧是 20

- (void)test {
  int a = 20;
  [xxx runAsyncFunc:^{
    a = a + 10;
    NSLog(@"%d",a); // 30
  }];
}

但是到 cpp 里,如果闭包是引用捕获,就会有问题,下面的方法捕获到 a 的引用后,不会影响其生命周期,a 还是随着函数销毁销毁了。实际使用a 的时候,引用悬空了。

void test() {
  int a = 20;
  runAsyncFunc(base::BindLambda([&] {
    a = a + 10;	
    print("%d", a); // 乱码; 引用捕获
  });
}

如果使用值引用,可以正常获取到值,但是使用的是a 的副本,确实是不会修改到外部的a

void test() {
  int a = 20;
  runAsyncFunc(base::BindLambda([=] {
    int b = a + 10;
    print("%d", b); // 30;值捕获
  });
}

如果真想修改外部的 a,可以把 a 变成共享指针,这样闭包捕获 a 后,会增加 a 的引用计数,推迟a 的释放

auto is_reported_download_first_data = std::make_shared<bool>(false);

void test() {
  auto a = std::make_shared<int>(20);
  runAsyncFunc(base::BindLambda([=] {
    *a = *a + 10;
    print("%d", *a); // 30;捕获共享指针
  });
}

至于 oc 的 block,其实函数结束时,a 也释放了。
能正常执行,其实是因为在 block 内部创建了一个 a 的副本,确实没有真正修改到外部的 a

- (void)test {
  int a = 20;
  [xxx runAsyncFunc:^{
    a = a + 10; // 这个 a 其实是副本,不是外面的 a,有兴趣的可以打印 a 的地址验证
    NSLog(@"%d",a); // 30
  }];
}

如果 oc 中想要真的修改的外部的 a,其实也有办法,就是用__block 修饰

- (void)test {
  __block int a = 20;
  [xxx runAsyncFunc:^{
    a = a + 10; // 这个 a 其实是副本,不是外面的 a,有兴趣的可以打印 a 的地址验证
    NSLog(@"%d",a); // 30
  }];
}

总结

Objective-C 的 Block 捕获机制

在Objective-C中,Block会自动管理被捕获变量的生命周期。具体来说:

  1. 值捕获:当一个局部变量被Block捕获时,Block会创建该变量的一个副本(即值捕获)。这意味着即使原始变量的作用域结束,Block中仍然保留一份有效的副本。

  2. 引用捕获:对于对象类型(如id或指针),Block默认捕获的是引用,而不是值。但由于ARC(Automatic Reference Counting)的存在,只要Block本身存活,被捕获的对象也会被正确地保留和释放。

因此,在Objective-C中,无论是值捕获还是引用捕获,Block都能确保被捕获的变量在其生命周期内是安全的。

C++ 的 Lambda 捕获机制

相比之下,C++ 的 Lambda 表达式对变量的捕获方式更为严格,且不会自动延长被捕获变量的生命周期。以下是两种捕获方式的具体行为:

  1. 值捕获([=][a]):
  • 当使用值捕获时,Lambda会在捕获时创建变量的一个副本。这个副本的生命周期独立于原始变量。

  • 因此,即使原始变量的作用域结束,Lambda中的副本仍然有效。

  1. 引用捕获([&][&a]):
  • 当使用引用捕获时,Lambda捕获的是变量的引用,而不是值。

  • 如果原始变量的作用域结束,其内存可能被释放,而Lambda仍然持有对该内存的引用。此时访问该引用会导致未定义行为(通常是乱码或崩溃)。