本章深入探讨了 C++ Lambda 表达式的特性、陷阱以及与 std::bind 的对比,展示了 Lambda 如何成为现代 C++ 编程的游戏规则改变者。

条款三十一:避免使用默认捕获模式

默认捕获模式([=][&])虽然方便,但极易引发悬空引用或误导开发者,存在严重的安全隐患。

  • 按引用捕获 ([&]) 的危险

    • 容易导致悬空引用。如果 Lambda 的生命周期超过了被捕获的局部变量或形参的生命周期,闭包内的引用将失效。

    • 建议:显式列出要捕获的变量(如 [&divisor]),这能提醒开发者关注该变量的生命周期。

  • 按值捕获 ([=]) 的陷阱

    • 悬空指针风险:如果按值捕获的是指针(包括隐式的 this 指针),虽然指针本身被拷贝了,但其指向的对象可能已被销毁,导致悬空指针访问。

      • 特别注意 this 指针:在成员函数中,[=] 会隐式捕获 this 指针,导致 Lambda 访问成员变量时实际上是通过 this-> 访问的。如果 Lambda 执行时对象已被销毁,将引发未定义行为。

    • 误导性[=] 可能让人误以为 Lambda 是独立的(self-contained),但实际上它可以引用不可被捕获的 static 变量或全局变量,导致行为受外部状态影响。

    • 解决方案:在 C++14 中使用广义捕获(初始化捕获)将成员变量拷贝到闭包中([divisor = divisor]),或在 C++11 中先拷贝到局部变量再捕获局部副本。

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

C++11 的 Lambda 捕获无法处理只可移动的对象(如 std::unique_ptr),C++14 引入了初始化捕获(Init Capture) 完美解决了这一问题。

  • C++14 初始化捕获

    • 允许在捕获列表中指定新数据成员的名称和初始化表达式(如 [pw = std::move(pw)])。

    • = 左边是闭包类成员名,右边是 Lambda 定义作用域内的表达式。

    • 实际上是广义 Lambda 捕获,不仅限于移动,还可以用于任何初始化逻辑。

  • C++11 的模拟方案

    • 如果必须在 C++11 中实现移动捕获,可以使用 std::bind 结合 Lambda。

      • 将要移动的对象通过 std::move 传递给 std::bind(bind 对象会移动构造该参数)。

      • Lambda 接收该对象作为参数(bind 对象调用时会将其传递给 Lambda)。

    • 或者手写一个函数对象类。

条款三十三:对 auto&& 形参使用 decltypestd::forward 它们

C++14 引入了泛型 Lambda(Generic Lambdas),允许在参数中使用 auto。在泛型 Lambda 中实现完美转发需要技巧。

  • 泛型 Lambda 的原理:闭包类的 operator() 是一个模板函数。

  • 完美转发的需求:当 Lambda 只是单纯转发参数时(如 [](auto&& x) { return f(x); }),为了保留参数的左值/右值属性,需要使用 std::forward

  • 如何获取类型 T:在泛型 Lambda 内部没有显式的模板参数 T 可用。但可以通过 decltype(x) 获取参数的类型。

  • 实现方式

    • std::forward<decltype(x)>(x)

    • 无论 x 是左值还是右值,decltype(x) 都能产生正确的类型(左值引用或右值引用),配合 std::forward 能够正确转发。

    • 对于可变参数包:std::forward<decltype(params)>(params)...

条款三十四:考虑 Lambda 而非 std::bind

std::bind 是 C++98/TR1 时代的产物,在 C++11/14 时代,Lambda 几乎总是更好的选择。

  • 可读性:Lambda 代码更加直观,逻辑清晰;而 std::bind 需要理解占位符 _1, _2 等魔法,且代码结构(如嵌套 bind)晦涩难懂。

  • 求值时机

    • std::bind 的参数在绑定时求值(除非嵌套 bind),这可能导致意料之外的行为(如时间参数被提前固定)。

    • Lambda 的参数在调用时求值,更符合直觉。

  • 重载处理:对于重载函数,std::bind 无法自动推导,必须显式转换函数指针类型,极其繁琐且脆弱;Lambda 则能自动正确匹配。

  • 性能:Lambda 生成的闭包类通常能被编译器内联优化;而 std::bind 返回的函数对象通过函数指针调用,难以内联。

  • std::bind 仅存的用途(C++11)

    • 模拟移动捕获:在 C++11 中 Lambda 不支持移动捕获,std::bind 可以作为替代方案。

    • 多态函数对象:C++11 Lambda 参数类型固定,而 bind 对象可以接受任意符合类型的参数。但 C++14 的泛型 Lambda 已经解决了这个问题。

  • 结论:从 C++14 开始,std::bind 已无合理用武之地;在 C++11 中也应尽量优先使用 Lambda。