文章

条款32:使用初始化捕获来移动对象到闭包中

通过初始化捕获,可将可移动对象(如unique_ptr)安全高效地移动进lambda闭包,避免拷贝开销。

条款32:使用初始化捕获来移动对象到闭包中

条款32:使用初始化捕获来移动对象到闭包中

核心问题

C++11 中,lambda 无法将对象移动进闭包,只能:

  • 按值捕获(复制)
  • 按引用捕获(依赖外部生命周期)

这在以下两种情况下非常不便:

  • 捕获的对象是 只能移动 的(如 std::unique_ptr, std::future
  • 捕获的对象 复制开销大(如 std::vector, std::string),但移动代价低

C++14 解决方案:初始化捕获(Init Capture)

初始化捕获又称“通用 lambda 捕获(Generalized Lambda Capture)”,引入于 C++14。

语法:

1
[变量名 = 初始化表达式]

示例:移动 unique_ptr 进闭包

1
2
3
4
5
auto pw = std::make_unique<Widget>();

auto func = [pw = std::move(pw)] {
    return pw->isValidated() && pw->isArchived();
};

更简洁的写法:

1
2
3
auto func = [pw = std::make_unique<Widget>()] {
    return pw->isValidated() && pw->isArchived();
};
  • = 左边:闭包内成员变量名
  • = 右边:定义 lambda 时的作用域表达式

C++11 如何模拟初始化捕获?

方法一:手写类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 类 IsValAndArch 实现一个可调用对象(函数对象)
// 用于判断某个 Widget 是否已验证并归档
class IsValAndArch {
public:
    // 定义数据成员类型:一个只能移动的智能指针
    using DataType = std::unique_ptr<Widget>;

    // 构造函数,接受一个右值引用,并将其移动到成员变量中
    explicit IsValAndArch(DataType&& ptr)
    : pw(std::move(ptr)) {}  // std::move 确保唯一所有权转移到类内

    // 函数调用操作符,使该类行为类似函数(lambda 替代品)
    // 本函数为 const,因此不能修改 pw,也保证了线程安全
    bool operator()() const {
        // 调用 Widget 中的接口函数,返回逻辑判断结果
        return pw->isValidated() && pw->isArchived();
    }

private:
    DataType pw;  // 保存 Widget 的独占所有权
};

// 使用 std::make_unique 创建 Widget 实例,并立即构造 IsValAndArch 对象
// 然后调用其 operator(),获取结果
auto func = IsValAndArch(std::make_unique<Widget>())();
// 等价于:
// auto obj = IsValAndArch(std::make_unique<Widget>());
// auto result = obj();

lambda 和类的关系本质上是什么?

1
auto f = [x]() { return x + 1; };

编译器会把上面 lambda 转换成这样一个类:

1
2
3
4
5
6
7
class Lambda {
    int x;
public:
    Lambda(int x_) : x(x_) {}
    int operator()() const { return x + 1; }
};
Lambda f(5);
  • 每个 lambda 本质上就是一个带有 operator() 的类对象,捕获变量就像类的成员变量。

方法二:使用 std::bind + lambda 引用参数

1
2
3
4
5
6
7
8
9
10
std::vector<double> data = ...;  // 要移动进闭包的对象,可能很大,复制开销高

auto func = std::bind(
    [](const std::vector<double>& data) {
        // 这里的 data 是传入的参数(通过 const 引用),实际上是 bind 内部保存的副本
        // 可以安全读取 data,而不用担心外部对象被修改或销毁
    },
    std::move(data)  // 将 data 移动到 bind 对象中,避免复制,完成“伪移动捕获”
);
// func 是一个可调用对象,调用时会传递 bind 内部保存的 data 副本给 lambda
  • std::move(data):将 data 资源移动到 std::bind 生成的函数对象中,避免复制开销。
  • lambda 的参数 const std::vector<double>& data:引用的是 bind 对象内部保存的移动副本,保证数据有效且只读。

若 lambda 为 mutable,可以传非常量引用:

1
2
3
4
5
6
auto func = std::bind(
    [](std::vector<double>& data) mutable {
        // 可以修改 data 副本
    },
    std::move(data)
);

示例:模拟 unique_ptr 捕获

1
2
3
4
5
6
auto func = std::bind(
    [](const std::unique_ptr<Widget>& pw) {
        return pw->isValidated() && pw->isArchived();
    },
    std::make_unique<Widget>()
);

注意事项

描述
C++11 限制不能捕获表达式结果(不能写 [x = foo()]
bind 对象会持有捕获对象的副本,与 lambda 生命周期一致
可修改性默认 lambda 为 const,若需修改绑定值,需加 mutable
移动成本bind 对象中的实参为 值存储,右值会移动进来,左值会复制
可读性bind 用法不如 lambda 简洁直观,建议仅用于 C++11 限制场景
本文由作者按照 CC BY 4.0 进行授权