std::unique_ptr和lambda表达式混用踩坑日记

一.unique_ptr的 引用捕获 vs 转移所有权

1.问题

我们知道unique_ptr是c++的一种不可拷贝的类型,即以下操作是非法的:

std::unique_ptr<int> p1 = std::make_unique<int>(10);
std::unique_ptr<int> p2 = p1;  // invalid, Call to implicitly-deleted copy constructor of 'unique_ptr'
std::unique_ptr<int> p3(p1);  // invalid, Call to implicitly-deleted copy constructor of 'unique_ptr'

因此无法在lambda闭包中直接按值捕获unique_ptr

std::unique_ptr<int> p1 = std::make_unique<int>(10);
auto closure = [p1] {     // invalid, Call to implicitly-deleted copy constructor of 'unique_ptr'
    printf("p1 = %d \n", *p1);
};

有2种方式可以在lambda闭包中访问外部的unique_ptr引用捕获转移所有权
引用捕获和转移所有权都可以使得unique_ptr可以在lambda闭包中使用,区别在于:

2.引用捕获

本质上是获得了外部作用域unique_ptr变量的引用,其生命周期完全受外部作用域控制

std::unique_ptr<int> p1 = std::make_unique<int>(10);
auto closure = [&p1] {
    printf("p1 = %d \n", *p1);
};

缺陷:unique_ptr生命周期不受控,lambda闭包内部访问时可能已经被外部修改或释放。如下面这种情况:

{
    std::unique_ptr<int> ptr = std::make_unique<int>(10);
    std::thread([&ptr]() {
         printf("ohter thread  ptr = %d \n", *ptr); // run time error: Thread 2: EXC_BAD_ACCESS (code=1, address=0x0)
    }).detach();
    printf("main thread  ptr = %d \n", *ptr);
}

// console output: 
// main thread  ptr = 10 

3.转移所有权

将外部作用域unique_ptr的所有权转移到lambda闭包内部,生命周期完全由lambda闭包内部控制

std::unique_ptr<int> p1 = std::make_unique<int>(10);
auto closure = [p1 = std::move(p1)] {
    printf("p1 = %d \n", *p1);
};

缺陷:转移之后,外部将无法再使用这个unique_ptr。如下面这种情况:

{
    std::unique_ptr<int> ptr = std::make_unique<int>(10);
    std::thread([ptr = std::move(ptr)]() {
         printf("ohter thread  ptr = %d \n", *ptr);
    }).detach();
    printf("main thread  ptr = %d \n", *ptr); // run time error: Thread 1: EXC_BAD_ACCESS (code=1, address=0x0)
}

二、在lambda闭包使用转移所有权获取unique_ptr时导致的编译问题

1.问题

看个例子:

std::function<void()> func() {
    auto ptr = std::make_unique<int>(1);
    return [ptr = std::move(ptr)]() {
        printf("*ptr = %d \n", *ptr);
  };
}

编译错误

include/c++/v1/__memory/compressed_pair.h:46:9: error: call to implicitly-deleted copy constructor of '(lambda at……
copy constructor of ‘’ is implicitly deleted because field ‘’ has a deleted copy constructor
return ptr = std::move(ptr) {

分析错误的原因,首先要先了解c++的lambda表达式相关原理。

lambda表达式事实上是定义了一个匿名类,并立即构建了一个该匿名类的实例对象。这个类内部重写了运算符operator(),以便在使用的时候可以当成函数的方式去调用。

于是,上面例子中的代码可以写成等价于以下代码:

struct __anonymous {
    __anonymous(std::unique_ptr<int> ptr) : _ptr(std::move(ptr)) {}
    void operator()() {
        printf("*ptr = %d \n", *_ptr);
    }
private:
    std::unique_ptr<int> _ptr;
};

std::function<void()> func() {
  auto ptr = std::make_unique<int>(1);
  return __anonymous(std::move(ptr));
}

当然,这段代码也有近乎类似的编译错误

include/c++/v1/__memory/compressed_pair.h:46:9: error: call to implicitly-deleted copy constructor of ‘__anonymous’
copy constructor of ‘__anonymous’ is implicitly deleted because field ‘_ptr’ has a deleted copy constructor std::unique_ptr _ptr;

2.原因分析:

当类中包含有不可拷贝的成员变量的时候(如unique_ptr),这个类就无法自动生成默认的拷贝函数(因为这个类里面包含有不可拷贝的成员变量)。因此当需要对这个类的对象进行拷贝操作的时候,如:

__anonymous a1;  
__anonymous a2 = a1; 
__anonymous a3(a1)

就无法正确定义这个类,因此产生编译错误。

⚠️⚠️⚠️如果实际使用的时候并没有上述拷贝操作,那么编译也不会有问题。主要是有触发拷贝类对象的时候有问题。

这里这个例子为什么会调用类的拷贝?
我们看到func()方法返回的是一个std::function对象,在构造std::function对象的时候,这个lambda表达式所构造的匿名的类对象,就会触发拷贝操作。而使用lambda闭包来构造std::function的时候,拷贝操作是无法避免的。

3.解决方案:

1.直接返回lambda表达式,而非构造std::function

auto func() {
    auto ptr = std::make_unique<int>(1);
    return [ptr = std::move(ptr)]() {
        printf("*ptr = %d \n", *ptr);
  };
}

⚠️方法1并没有从根本上解决问题,只是避免了去构造std::function,如果有强制使用std::function的情况,错误仍然会出现

2.使用std::reflambda表达式包装成std::function(只是解决了编译问题,运行仍然报错)

std::function<void()> func() {
    auto ptr = std::make_unique<int>(1);
    auto lambda = [ptr = std::move(ptr)]() {
        printf("*ptr = %d \n", *ptr);
    };
    return std::ref(lambda);
}

❌这里运行会报错,因为return之后,auto lambda这个局部变量就会被释放掉,相对应的这个匿名类的内部被std::move进来的这个std::unique_ptr也会被释放掉,因此外部在调用这个std::function的时候, std::unique_ptr已经失效了

3.待补充

参考资料:

https://taylorconor.com/blog/noncopyable-lambdas/

你可能感兴趣的:(C/C++,c++,开发语言)