函数对象与lambda表达式

1 什么是函数对象

1.1 函数对象

  函数对象是任何可以被当做函数来使用的对象。

class Adder {
	Adder(int n) : n_(n) {}
	int operator()(int x) const { return x + n_; }
private:
	int n_;
};

  上述定义了一个普通的类型,唯一特别的地方是在类中定义了一个operator(),这个运算法符允许我们想调用函数一样使用小括号的语法。如:Adder add2(2);此时,得到的add2就有可以当一个函数来使用了,如果我们写下add2(5)的话,就得到结果7。由于运算符重载的作用,这个表达式实际作用相当于add2.operator()(5)。需要注意的地方是operator()通常是const成员函数,即应显式声明调用这个成员函数不会修改对象的状态。

1.2 函数对象与STL算法的结合

  函数对象可用于自定义算法行为,如排序、过滤等:
例程:

#include <vector>
#include <iostream>
#include <algorithm>
using namespace std;

template <typename T>
class LessThan {
public:
    LessThan() = default;
    bool operator()(T a, T b) {
        return a < b;
    }
};

int main() {
    vector<int> vec{5, 9, 7, 1, 6, 3, 2, 8};
    LessThan<int> ls;
    sort(vec.begin(), vec.end(), ls);

    for (const auto &x : vec) {
        cout << x << endl;
    }
    return 0;
}

  这里,我们介绍了函数对象以及函数对象的使用场景,但是,上述例程中,使用函数对象与std::sort实现升序排序的代码略显冗长,有没有其他简洁的方法呢?其实是有的,我们可以改写成如下:

sort(vec.begin(), vec.end(), [](int a, int b) {return a < b;});

  这里的写法就是C++的lambda表达式。

2 lambda表达式

2.1 基本语法

  lambda表达式的完整语法为:

[捕获列表](参数列表) mutable/异常说明符 -> 返回类型 { 函数体 }
  • 捕获列表:决定外部变量的访问方式(值捕获、引用捕获或者混合捕获)
  • 参数列表:与普通函数参数类似,可省略(无参数时)
  • mutable:允许修改修改按值捕获的变量(默认lambda表达式为const函数)
  • 返回类型:可自动推导,但是多分支返回需要显示指定返回值类型
auto sum = [](int a, int b) -> int { return a + b; };  //有参数,显示指定返回值
auto print = [] { std::cout << "Hello"; }; //无参数,返回void

2.2 捕获列表的规则

  • 值捕获[x]:复制变量到闭包,外部修改不影响内部(需mutable才可以修改副本)
  • 引用捕获[&x]: 直接操作外部变量,修改影响原值
  • 隐式捕获
    • [=]:按值捕获所有变量(包括this,但是在C++20开始已弃用)
    • [&]:按引用捕获所有变量。
  • 混合捕获:如[=, &x](值捕获全部,x按引用捕获)或[&, x](引用捕获全部,x按值捕获)
  • 特殊方式捕获
    • [this]:在成员函数里捕获当前对象的this指针,以便在lambda表达式里访问当前对象的成员。这里语法是属于值捕获,但实际效果更接近“按引用捕获”。
    • [*this]:在成员函数里捕获当前对象的一个副本,以便在lambda表达式里访问这个对象副本的成员
    • 用“变量 = 表达式”捕获表达式的值到指定的变量里
    • 用“&变量 = 表达式”捕获表达式的引用到指定的变量里。

示例:

int x = 1, y = 2;
auto lambda = [=, &y]() mutable {
	x++;   //修改副本,不影响外部x
	y++;   //修改外部y
};

  此外,需要注意的一点是,在按引用捕获时你仍可“修改”捕获的变量,因为此时我们修改的不是引用,而是引用指向的对象:也就是说不需要mutable就可以在lambda表达式里修改按引用捕获的对象的源对象。
示例

int x = 1;
auto lambda = [&x]() /*mutable*/ { //无需mutable就可以修改引用的源对象
	x++;   //修改外部x
};

2.3 lambda的实现原理

  编译器会将lambda转换为一个匿名类,重载operator

//lambda: [x](int a) { return a + x; }
class Closure {
private:
  int x;
public:
  Closure(const int& x) : x(x) {}
  int operator()(int a) const { return a + x; }
};
//lambda: [&x](int a) { return a + x; }
class Closure {
private:
  int &x;
public:
  Closure(int &x) : x(x) {}
  int operator()(int a) const { return a + x; }
}

闭包对象在运行时创建,捕获的变量作为类的成员。也就是说,按值捕获会把被捕获的变量的值存在函数对象里,而按引用捕获会把被捕获变量的引用存到函数对象里。

2.4 易错点与注意事项

1.捕获变量的声明周期

  • 悬挂引用:若lambda捕获局部变量的引用,而变量已经销毁,将导致未定义行为。
auto create_lambda() {
  int x = 10;
  return [&x]() { return x; }; //x 已经销毁,调用时奔溃
}

2.返回类型推导的陷阱

  • 自动推导失败:若lambda体包含多分支返回类型不一致的语句,需要显示指定返回类型.
auto lambda = [](int x){
  if (x > 0) {
    return 1.0;
  } else {
    return 0;  //需同一返回类型
  } 
}
//g++ test3.cpp  --  error: inconsistent types ‘double’ and ‘int’ deduced for lambda return type

3.线程安全问题

  • 引用捕获与多线程:若多个线程通过引用捕获共享变量,需要同步机制避免数据竞争。

2.5 高级特性

1.初始化捕获: 允许在捕获时初始化变量(支持右值)

auto ptr = std::make_unique<int>(10);
auto lambda = [v = std::move(ptr)] { return *v; };

2.泛型lambda:参数支持auto类型推导

auto add = [](auto a, auto b) { return a + b; };
posted @ 2025-03-16 17:06  ydqun  阅读(47)  评论(0)    收藏  举报