C++11中的lambda

一、语法

  [capture](parameters) -> return_type {
      function_body
  };

1) 各部分的含义如下:

  • [capture]:捕获列表,用于指定 lambda 表达式可以使用的外部变量。
  • (parameters):参数列表,和普通函数类似,定义lambda表达式的参数, 参数类型可以是引用、指针、值等。
  • -> return_type(可选):指定返回类型,如果可以自动推导则可以省略。
  • {}:函数体,定义 lambda 表达式的逻辑。

2) 捕获方式

  • 空捕获([]): 在lambda内部无法使用其外层上下文中的任何局部名字。
  • 捕获所有变量的副本 ([=]):通过值隐式捕获, 自动捕获所有在lambda体中使用的外部变量的副本。
  • 捕获所有变量的引用 ([&]):通过引用隐式捕获, 自动捕获所有在lambda体中使用的外部变量的引用。
  • 按值捕获 ([x]):显式捕获;将外部变量x按值传递给lambda表达式,即生成lambda时会复制该变量的当前值。修改lambda中的变量不会影响外部变量。其中x是一个捕获列表,可以是一个外部变量,也可以是多个外部变量。
  • 按引用捕获 ([&x]):显式捕获;将外部变量x的引用传递给lambda表达式,即lambda中对变量的修改会影响外部的变量。其中x是一个捕获列表,可以是一个外部变量,也可以是多个外部变量。
  • [=, &x] :x是一个捕获列表,可以是一个外部变量,也可以是多个外部变量。对于名字没有出现在捕获列表中的局部变量,通过值隐式捕获。捕获列表中不允许包含this。列出的名字必须以&为前缀。捕获列表中的变量名通过引用的方式捕获。([=, this]是一个错误,因为=已经通过复制隐式捕获this,而[&, this]中的&则表示引用捕获,并且不隐式捕获this)
  • [&, x] :x是一个捕获列表,可以是一个外部变量,也可以是多个外部变量。对于名字没有出现在捕获列表中的局部变量,通过引用隐式捕获。捕获列表中可以出现this。列出的名字不能以&为前缀。捕获列表中的变量名通过值的方式捕获。
    auto func = [=, this](){}; //waring: explicit by-copy capture of “this” redunant with by-copy capture default [enabled by default] ==>  显式复制捕获“this”与默认隐式复制捕获重叠(默认启用)
    

3) 可变性(mutable关键字)

默认情况下,lambda表达式按值捕获的变量是不可修改的。如果需要修改捕获的变量,可以使用 mutable 关键字。注意:mutable影响的是捕获的变量,通过引用传递的参数,哪怕没有mutable关键字也是可以修改的。(通常情况下,人们不希望修改函数对象(闭包)的状态,因此默认设置为不可修改。换句话说,生成的函数对象的operator()()是一个const成员函数。只有在极少数情况下,如果我们确实希望修改状态(注意,不是修改通过引用捕获的变量的状态),则可以把1ambda声明成mutable的。)

  int x = 10;
  
  //值捕获不能修改
  auto modify = [x]() {
    x += 5;  //error: assignment of read-only variable 'x'
    return x;
  };  
  
  auto modify = [x]() mutable {
      x += 5;
      return x;
  };

  //引用捕获可以修改
  auto modify2 = [&x]() {
      x += 5;
      return x;
  };

  int result = modify();  // result=15, x=10
  int result2 = modify2();  // result=15, y=15  

4) 调用与返回

lambda传递参数的规则与向函数传递参数是一样的, 从lambda返回结果也是如此。实际上,除了关于捕获的规则之外,lambda的大多数规则都是从函数和类借鉴而来的。然而,有两点需要注意:

  • 如果一条lambda表达式不接受任何参数,则其参数列表可被忽略。因此,lambda表达式的最简形式是 []{}。
  • lambda表达式的返回类型能由lambda表达式本身推断得到,然而函数无法做到这一点。

如果在lambda的主体部分不包含return语句,则该lambda的返回类型是 void。如果lambda的主体部分只包含一条return语句,则该lambda的返回类型是该return 表达式的类型。其他情况下,我们必须显式地提供一个返回类型。

  [&]{ f(y); }  // return type is void
  auto z1 = [=](int x){ return x+y; }  //return type is double
  auto z2 = [=, y]{ if (y) return 1; else return 2; } // error: body too complicated  for return type deduction
  auto z3 =[y]() { return 1 : 2; }   // return type is int
  auto z4 = [=, y]()>int { if (y) return 1; else return 2; }  // OK: explicit return type

二、示例

  //不捕获任何外部变量
  auto add = [](int a, int b) {
      return a + b;
  };
  int result = add(2, 3);

  //捕获所有外部变量的引用
  int x = 10, y = 20;
  auto increment = [&]() {
      x += 5;
      y += 5;
  };
  increment();  // x = 15, y = 25

  //如果返回类型无法自动推导,可以显式指定
  auto divide = [](int a, int b) -> double {
      return static_cast<double>(a) / b;
  };
  double result = divide(10, 3);  // result = 3.33333  

  //值捕获 lambda 表达式内部持有的是值的副本,不受外部变量变化的影响。
  int value = 42;
  auto func= [value]() { std::cout << value << std::endl;};
  func(); // 输出42
  value = 100;
  func(); // 仍然输出42

三、lambda的用途

1) 结合STL算法使用

  //Lambda表达式通常与STL算法结合使用,如 std::sort、std::for_each 等
  std::vector<int> vec = {1, 2, 3, 4, 5};
  std::for_each(vec.begin(), vec.end(), [](int &n) { n *= 2; });
  // vec 变为 {2, 4, 6, 8, 10}

2) 回调函数

  • Lambda表达式可以用于回调函数,简化了传递临时逻辑的过程。

3) 并发编程

  //在多线程或并发编程中,lambda表达式可以方便地传递临时任务到线程或任务队列中
  std::thread t([]() {
      std::cout << "Hello from thread!" << std::endl;
  });
  t.join();

四、注意事项

1) lambda的生命周期

lambda的生命周期可能比它的调用者更长。当我们把lambda传递给另外一个线程或者被调用者把 lambda 存在别处以供后续使用时,这种情况就会发生。例如:

  void setup(Menu& m)
  {
      // ...
      Point p1, p2, p3;
      
      // compute positions of p1, p2, and p3
      m.add("draw triangle",[&]{ m.draw(p1,p2,p3); }); // probable disaster

      // ...
  }

假如 add() 负责把一个(名字,动作)对添加到菜单中,并且 draw() 操作是有效的,则上述程序无异于埋下了一颗定时炸弹:setup()调用完成之后 --> 也许要到好几分钟之后 --> 用户点了draw triangle按钮,此时lambda将会试图访问一个早已不存在的局部变量。如果在某些程序中lambda需要向通过引用捕获的变量写入内容,情况就更糟糕了。
因此,如果我们发现 lambda 的生命周期可能比它的调用者更长,就必须确保所有局部信息(如果有的话)都被拷贝到闭包对象中,并且这些值应该通过return机制或者适当的实参返回。对于setup()的例子来说,很容易做到这一点:

  m.add("draw triangle", [=]{ m.draw(p1,p2,p3); });

2) lambda关于捕获this的陷阱

  • lambda表达式可以捕获对象的this指针,但当对象生命周期结束,继续通过lambda访问会导致野指针问题。C++17引入了捕获this的副本来解决这个问题。
    class Timer {
        int interval;
        function<void()> callback;
    public:
        Timer(int ms) : interval(ms) {} 
        
        void setTimeout() {
            // ⚠️ 危险:这里使用[this]捕获可能导致野指针
            auto task = [this]() {
                callback();  // 💥 如果Timer对象已销毁, 这里会崩溃!
            };
            scheduler.schedule(interval, task);
        }
    };
    
    /**
     * 优化后代码:
     * lambda捕获的对象副本与lambda对象具有相同的生命周期。
     * 被捕获的副本是作为lambda对象的一个成员存在的。只要 lambda 对象还活着,这个副本就会一直存在。
     * 当 lambda 对象最终被销毁时,这个副本也会跟着被销毁。
     */
    class Timer {
        // ... 其他代码不变 ...
        void setTimeout() {
            // 🔑 使用[*this]进行值捕获, 创建Timer对象的完整副本
            // 🛡️ 这样即使原Timer对象被销毁, lambda 也能安全运行
            auto task = [*this]() mutable {  
                // ✨ 在Timer副本上调用callback,完全安全
                // 💫 mutable关键字允许修改捕获对象的副本
                callback();  
            };
            // 📅 将任务提交给调度器
            // 🔄 调度器会持有task直到执行完成
            scheduler.schedule(interval, task);
        }
    };    
    
  • 在多线程环境中,lambda表达式可能会在不同的线程中执行,而this指针指向的对象可能在其他线程中被销毁。这种情况下,即使对象没有被立即销毁,也可能因为并发访问而导致数据竞争或竞态条件。
    /**
     * Worker对象可能在其他线程中被销毁,而lambda表达式仍然在尝试访问它,导致未定义行为。
     * 为了避免这种情况,我们需要确保在lambda表达式执行之前,对象不会被销毁;
     * 并且在多线程环境中采取适当的同步措施。
    */
	class Worker {
	public:    
		void startTask() {        
			auto lambda = [this]() {    
	            // 可能在其他线程中执行        
			    processTask();        
		    };        
		    thread_pool.submit(lambda);    
		}  
		void processTask() {        
		    // 处理任务   
		}
	};
  • 当类中有特殊的成员,比如智能指针或者互斥量
    class ResourceManager {
        // 🔒 独占式智能指针,不支持复制
        unique_ptr<Resource> resource;
        // 🔐 互斥锁对象,也不支持复制
        mutex mtx;
        
        void processAsync() {
            // ⚠️ 以下代码存在严重问题:
            auto task = [*this]() {  // 💥 这里会尝试复制整个对象!
                // ❌ 错误1: mtx是副本,不同线程会获取不同的锁,失去了互斥作用
                lock_guard<mutex> lock(mtx);
                // ❌ 错误2: unique_ptr不支持复制,编译会失败
                resource->process();
            };
            // 📤 提交任务到线程池
            threadPool.submit(task);
        }
    };
    
    // ✅ 正确的实现方式:
    class ResourceManager {
        // 👥 改用支持共享的智能指针
        shared_ptr<Resource> resource;
        
        // 🔐 使用静态互斥锁确保真正的线程安全
        static mutex& getMutex() { 
            static mutex mtx; 
            return mtx; 
        }
        
        void processAsync() {
            // 📦 只捕获需要的资源
            auto res = resource;  // 👍 shared_ptr支持复制
            
            auto task = [res]() {  // ✨ 显式捕获所需资源
                // ✅ 所有线程使用同一个互斥锁
                lock_guard<mutex> lock(ResourceManager::getMutex());
                // 🚀 安全地访问共享资源
                res->process();
            };
            // 📤 提交到线程池
            threadPool.submit(task);
        }
    };
    
  • 使用 [*this] 捕获时要注意事项
    • 确保类的所有成员都是可复制的;
    • 对于不可复制的成员(如 mutex), 考虑使用静态成员或其他替代方案;
    • 对于独占型智能指针(unique_ptr),考虑改用 shared_ptr;
    • 如果只需要部分成员,最好显式捕获这些成员而不是整个对象;
    • 注意捕获对象的大小,避免不必要的性能开销";
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值